A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

267 lines
8.4 KiB

// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reflection;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media;
namespace Avalonia.Controls.Shapes
{
public abstract class Shape : Control
{
public static readonly StyledProperty<IBrush> FillProperty =
AvaloniaProperty.Register<Shape, IBrush>("Fill");
public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Shape, Stretch>("Stretch");
public static readonly StyledProperty<IBrush> StrokeProperty =
AvaloniaProperty.Register<Shape, IBrush>("Stroke");
public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>>("StrokeDashArray");
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Shape, double>("StrokeThickness");
private Matrix _transform = Matrix.Identity;
private Geometry _definingGeometry;
private Geometry _renderedGeometry;
static Shape()
{
AffectsMeasure(StretchProperty, StrokeThicknessProperty);
AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty);
}
public Geometry DefiningGeometry
{
get
{
if (_definingGeometry == null)
{
_definingGeometry = CreateDefiningGeometry();
}
return _definingGeometry;
}
}
public IBrush Fill
{
get { return GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
public Geometry RenderedGeometry
{
get
{
if (_renderedGeometry == null)
{
if (DefiningGeometry != null)
{
_renderedGeometry = DefiningGeometry.Clone();
_renderedGeometry.Transform = new MatrixTransform(_transform);
}
}
return _renderedGeometry;
}
}
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
public IBrush Stroke
{
get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
public AvaloniaList<double> StrokeDashArray
{
get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
public double StrokeThickness
{
get { return GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
public PenLineCap StrokeDashCap { get; set; } = PenLineCap.Flat;
public PenLineCap StrokeStartLineCap { get; set; } = PenLineCap.Flat;
public PenLineCap StrokeEndLineCap { get; set; } = PenLineCap.Flat;
public PenLineJoin StrokeJoin { get; set; } = PenLineJoin.Miter;
public override void Render(DrawingContext context)
{
var geometry = RenderedGeometry;
if (geometry != null)
{
var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray),
StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin);
context.DrawGeometry(Fill, pen, geometry);
}
}
/// <summary>
/// Marks a property as affecting the shape's geometry.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks>
protected static void AffectsGeometry<TShape>(params AvaloniaProperty[] properties)
where TShape : Shape
{
foreach (var property in properties)
{
property.Changed.Subscribe(e =>
{
var senderType = e.Sender.GetType().GetTypeInfo();
var affectedType = typeof(TShape).GetTypeInfo();
if (affectedType.IsAssignableFrom(senderType))
{
AffectsGeometryInvalidate(e);
}
});
}
}
protected abstract Geometry CreateDefiningGeometry();
protected void InvalidateGeometry()
{
this._renderedGeometry = null;
this._definingGeometry = null;
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
// This should probably use GetRenderBounds(strokeThickness) but then the calculations
// will multiply the stroke thickness as well, which isn't correct.
var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch);
if (_transform != transform)
{
_transform = transform;
_renderedGeometry = null;
}
return size;
}
internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)
{
Size shapeSize = new Size(shapeBounds.Right, shapeBounds.Bottom);
Matrix translate = Matrix.Identity;
double desiredX = availableSize.Width;
double desiredY = availableSize.Height;
double sx = 0.0;
double sy = 0.0;
if (Stretch != Stretch.None)
{
shapeSize = shapeBounds.Size;
translate = Matrix.CreateTranslation(-(Vector)shapeBounds.Position);
}
if (double.IsInfinity(availableSize.Width))
{
desiredX = shapeSize.Width;
}
if (double.IsInfinity(availableSize.Height))
{
desiredY = shapeSize.Height;
}
if (shapeBounds.Width > 0)
{
sx = desiredX / shapeSize.Width;
}
if (shapeBounds.Height > 0)
{
sy = desiredY / shapeSize.Height;
}
if (double.IsInfinity(availableSize.Width))
{
sx = sy;
}
if (double.IsInfinity(availableSize.Height))
{
sy = sx;
}
switch (Stretch)
{
case Stretch.Uniform:
sx = sy = Math.Min(sx, sy);
break;
case Stretch.UniformToFill:
sx = sy = Math.Max(sx, sy);
break;
case Stretch.Fill:
if (double.IsInfinity(availableSize.Width))
{
sx = 1.0;
}
if (double.IsInfinity(availableSize.Height))
{
sy = 1.0;
}
break;
default:
sx = sy = 1;
break;
}
var transform = translate * Matrix.CreateScale(sx, sy);
var size = new Size(shapeSize.Width * sx, shapeSize.Height * sy);
return (size, transform);
}
private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
{
var control = e.Sender as Shape;
if (control != null)
{
// If the geometry is invalidated when Bounds changes, only invalidate when the Size
// portion changes.
if (e.Property == BoundsProperty)
{
var oldBounds = (Rect)e.OldValue;
var newBounds = (Rect)e.NewValue;
if (oldBounds.Size == newBounds.Size)
{
return;
}
}
control.InvalidateGeometry();
}
}
}
}