From a1ee3efab8e45d421a850cec8a93a8db744eab9a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 3 Sep 2015 15:58:15 +0200 Subject: [PATCH] More work on VisualBrush, not entirely correct yet. --- src/Perspex.Controls/Image.cs | 35 +---- src/Perspex.SceneGraph/Media/AlignmentX.cs | 29 +++++ src/Perspex.SceneGraph/Media/AlignmentY.cs | 29 +++++ .../Media/MediaExtensions.cs | 47 +++++++ src/Perspex.SceneGraph/Media/TileBrush.cs | 90 +++++++++++++ src/Perspex.SceneGraph/Media/VisualBrush.cs | 18 ++- .../Perspex.SceneGraph.csproj | 5 + src/Perspex.SceneGraph/RelativeRect.cs | 105 +++++++++++++++ .../Rendering/RendererBase.cs | 15 ++- .../Perspex.Direct2D1/Media/DrawingContext.cs | 32 +++-- .../Media/PerspexTextRenderer.cs | 4 +- .../Brushes/VisualBrushTests.cs | 120 +++++++++++++++++- .../Perspex.Direct2D1.RenderTests.csproj | 1 + 13 files changed, 480 insertions(+), 50 deletions(-) create mode 100644 src/Perspex.SceneGraph/Media/AlignmentX.cs create mode 100644 src/Perspex.SceneGraph/Media/AlignmentY.cs create mode 100644 src/Perspex.SceneGraph/Media/MediaExtensions.cs create mode 100644 src/Perspex.SceneGraph/Media/TileBrush.cs create mode 100644 src/Perspex.SceneGraph/RelativeRect.cs diff --git a/src/Perspex.Controls/Image.cs b/src/Perspex.Controls/Image.cs index 698d4dc206..5e10e5daab 100644 --- a/src/Perspex.Controls/Image.cs +++ b/src/Perspex.Controls/Image.cs @@ -57,7 +57,7 @@ namespace Perspex.Controls { Rect viewPort = new Rect(this.Bounds.Size); Size sourceSize = new Size(source.PixelWidth, source.PixelHeight); - Vector scale = CalculateScaling(this.Bounds.Size, sourceSize, this.Stretch); + Vector scale = this.Stretch.CalculateScaling(this.Bounds.Size, sourceSize); Size scaledSize = sourceSize * scale; Rect destRect = viewPort .CenterIn(new Rect(scaledSize)) @@ -95,41 +95,10 @@ namespace Perspex.Controls availableSize = new Size(availableSize.Width, this.Height); } - scale = CalculateScaling(availableSize, new Size(width, height), this.Stretch); + scale = this.Stretch.CalculateScaling(availableSize, new Size(width, height)); } return new Size(width * scale.X, height * scale.Y); } - - /// - /// Calculates the scaling for the image. - /// - /// The size available to display the image. - /// The pxiel size of the image. - /// The stretch mode of the control. - /// A vector with the X and Y scaling factors. - private static Vector CalculateScaling(Size availableSize, Size imageSize, Stretch stretch) - { - double scaleX = 1; - double scaleY = 1; - - if (stretch != Stretch.None) - { - scaleX = availableSize.Width / imageSize.Width; - scaleY = availableSize.Height / imageSize.Height; - - switch (stretch) - { - case Stretch.Uniform: - scaleX = scaleY = Math.Min(scaleX, scaleY); - break; - case Stretch.UniformToFill: - scaleX = scaleY = Math.Max(scaleX, scaleY); - break; - } - } - - return new Vector(scaleX, scaleY); - } } } \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Media/AlignmentX.cs b/src/Perspex.SceneGraph/Media/AlignmentX.cs new file mode 100644 index 0000000000..9ade324e18 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/AlignmentX.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Media +{ + /// + /// Describes how content is positioned horizontally in a container. + /// + public enum AlignmentX + { + /// + /// The contents align themselves with the left of the container + /// + Left, + + /// + /// The contents align themselves with the center of the container + /// + Center, + + /// + /// The contents align themselves with the right of the container + /// + Right, + } +} diff --git a/src/Perspex.SceneGraph/Media/AlignmentY.cs b/src/Perspex.SceneGraph/Media/AlignmentY.cs new file mode 100644 index 0000000000..24ef2de60d --- /dev/null +++ b/src/Perspex.SceneGraph/Media/AlignmentY.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Media +{ + /// + /// Describes how content is positioned vertically in a container. + /// + public enum AlignmentY + { + /// + /// The contents align themselves with the top of the container + /// + Top, + + /// + /// The contents align themselves with the center of the container + /// + Center, + + /// + /// The contents align themselves with the bottom of the container + /// + Bottom, + } +} diff --git a/src/Perspex.SceneGraph/Media/MediaExtensions.cs b/src/Perspex.SceneGraph/Media/MediaExtensions.cs new file mode 100644 index 0000000000..b5f67a7a43 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/MediaExtensions.cs @@ -0,0 +1,47 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Media +{ + using System; + + /// + /// Provides extension methods for Perspex media. + /// + public static class MediaExtensions + { + /// + /// Calculates scaling based on a value. + /// + /// The stretch mode. + /// The size of the destination viewport. + /// The size of the source. + /// A vector with the X and Y scaling factors. + public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize) + { + double scaleX = 1; + double scaleY = 1; + + if (stretch != Stretch.None) + { + scaleX = destinationSize.Width / sourceSize.Width; + scaleY = destinationSize.Height / sourceSize.Height; + + switch (stretch) + { + case Stretch.Uniform: + scaleX = scaleY = Math.Min(scaleX, scaleY); + break; + case Stretch.UniformToFill: + scaleX = scaleY = Math.Max(scaleX, scaleY); + break; + } + } + + return new Vector(scaleX, scaleY); + } + } +} diff --git a/src/Perspex.SceneGraph/Media/TileBrush.cs b/src/Perspex.SceneGraph/Media/TileBrush.cs new file mode 100644 index 0000000000..59160d70c0 --- /dev/null +++ b/src/Perspex.SceneGraph/Media/TileBrush.cs @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Media +{ + /// + /// Base class for brushes which display repeating images. + /// + public abstract class TileBrush : Brush + { + /// + /// Defines the property. + /// + public static readonly PerspexProperty AlignmentXProperty = + PerspexProperty.Register("ALignmentX", AlignmentX.Center); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty AlignmentYProperty = + PerspexProperty.Register("ALignmentY", AlignmentY.Center); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty DestinationRectProperty = + PerspexProperty.Register("DestinationRect", RelativeRect.Fill); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty SourceRectProperty = + PerspexProperty.Register("SourceRect", RelativeRect.Fill); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty StretchProperty = + PerspexProperty.Register(nameof(Stretch), Stretch.Uniform); + + /// + /// Gets or sets the horizontal alignment of a tile in the destination. + /// + public AlignmentX AlignmentX + { + get { return this.GetValue(AlignmentXProperty); } + set { this.SetValue(AlignmentXProperty, value); } + } + + /// + /// Gets or sets the horizontal alignment of a tile in the destination. + /// + public AlignmentY AlignmentY + { + get { return this.GetValue(AlignmentYProperty); } + set { this.SetValue(AlignmentYProperty, value); } + } + + /// + /// Gets or sets the rectangle on the destination in which to paint a tile. + /// + public RelativeRect DestinationRect + { + get { return this.GetValue(DestinationRectProperty); } + set { this.SetValue(DestinationRectProperty, value); } + } + + /// + /// Gets or sets the rectangle of the source image that will be displayed. + /// + public RelativeRect SourceRect + { + get { return this.GetValue(SourceRectProperty); } + set { this.SetValue(SourceRectProperty, value); } + } + + /// + /// Gets or sets a value controlling how the source rectangle will be stretched to fill + /// the destination rect. + /// + public Stretch Stretch + { + get { return (Stretch)this.GetValue(StretchProperty); } + set { this.SetValue(StretchProperty, value); } + } + } +} diff --git a/src/Perspex.SceneGraph/Media/VisualBrush.cs b/src/Perspex.SceneGraph/Media/VisualBrush.cs index 73feade478..d760004db6 100644 --- a/src/Perspex.SceneGraph/Media/VisualBrush.cs +++ b/src/Perspex.SceneGraph/Media/VisualBrush.cs @@ -6,20 +6,36 @@ namespace Perspex.Media { - public class VisualBrush : Brush + /// + /// Paints an area with an . + /// + public class VisualBrush : TileBrush { + /// + /// Defines the property. + /// public static readonly PerspexProperty VisualProperty = PerspexProperty.Register("Visual"); + /// + /// Initializes a new instance of the class. + /// public VisualBrush() { } + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. public VisualBrush(IVisual visual) { this.Visual = visual; } + /// + /// Gets or sets the visual to draw. + /// public IVisual Visual { get { return this.GetValue(VisualProperty); } diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index 765915b16a..ff8e0cf94e 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -57,10 +57,13 @@ + + + @@ -86,6 +89,7 @@ + @@ -98,6 +102,7 @@ + diff --git a/src/Perspex.SceneGraph/RelativeRect.cs b/src/Perspex.SceneGraph/RelativeRect.cs new file mode 100644 index 0000000000..fb688841ae --- /dev/null +++ b/src/Perspex.SceneGraph/RelativeRect.cs @@ -0,0 +1,105 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + /// + /// Defines a rectangle that may be defined relative to another rectangle. + /// + public struct RelativeRect + { + /// + /// A rectangle that represents 100% of an area. + /// + public static readonly RelativeRect Fill = new RelativeRect(0, 0, 1, 1, OriginUnit.Percent); + + /// + /// Initializes a new instance of the structure. + /// + /// The X position. + /// The Y position. + /// The width. + /// The height. + /// The unit of the rect. + public RelativeRect(double x, double y, double width, double height, OriginUnit unit) + { + this.Rect = new Rect(x, y, width, height); + this.Unit = unit; + } + + /// + /// Initializes a new instance of the structure. + /// + /// The rectangle. + /// The unit of the rect. + public RelativeRect(Rect rect, OriginUnit unit) + { + this.Rect = rect; + this.Unit = unit; + } + + /// + /// Initializes a new instance of the structure. + /// + /// The size of the rectangle. + /// The unit of the rect. + public RelativeRect(Size size, OriginUnit unit) + { + this.Rect = new Rect(size); + this.Unit = unit; + } + + /// + /// Initializes a new instance of the structure. + /// + /// The position of the rectangle. + /// The size of the rectangle. + /// The unit of the rect. + public RelativeRect(Point position, Size size, OriginUnit unit) + { + this.Rect = new Rect(position, size); + this.Unit = unit; + } + + /// + /// Initializes a new instance of the structure. + /// + /// The top left position of the rectangle. + /// The bottom right position of the rectangle. + /// The unit of the rect. + public RelativeRect(Point topLeft, Point bottomRight, OriginUnit unit) + { + this.Rect = new Rect(topLeft, bottomRight); + this.Unit = unit; + } + + /// + /// Gets the unit of the rectangle. + /// + public OriginUnit Unit { get; } + + /// + /// Gets the rectangle. + /// + public Rect Rect { get; } + + /// + /// Converts a into pixels. + /// + /// The size of the visual. + /// The origin point in pixels. + public Rect ToPixels(Size size) + { + return this.Unit == OriginUnit.Pixels ? + this.Rect : + new Rect( + this.Rect.X * size.Width, + this.Rect.Y * size.Height, + this.Rect.Width * size.Width, + this.Rect.Height * size.Height); + } + } +} diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererBase.cs index 6ad42eedb7..43e193dfd3 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererBase.cs @@ -33,10 +33,23 @@ namespace Perspex.Rendering /// The visual to render. /// An optional platform-specific handle. public virtual void Render(IVisual visual, IPlatformHandle handle) + { + this.Render(visual, handle, Matrix.Identity, Matrix.Identity); + } + + /// + /// Renders the specified visual with the specified transform. + /// + /// The visual to render. + /// An optional platform-specific handle. + /// The translation. + /// The transform. + public virtual void Render(IVisual visual, IPlatformHandle handle, Matrix translation, Matrix transform) { using (var context = this.CreateDrawingContext(handle)) { - this.Render(visual, context, Matrix.Identity, Matrix.Identity); + //context.PushTransform(translation * transform); + this.Render(visual, context, translation, transform); } ++this.RenderCount; diff --git a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs index f695e8335e..33571c34fc 100644 --- a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs @@ -90,7 +90,9 @@ namespace Perspex.Direct2D1.Media { if (pen != null) { - using (var d2dBrush = this.CreateBrush(pen.Brush)) + var size = new Rect(p1, p2).Size; + + using (var d2dBrush = this.CreateBrush(pen.Brush, size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(this.renderTarget)) { this.renderTarget.DrawLine( @@ -113,7 +115,7 @@ namespace Perspex.Direct2D1.Media { if (brush != null) { - using (var d2dBrush = this.CreateBrush(brush)) + using (var d2dBrush = this.CreateBrush(brush, geometry.Bounds.Size)) { GeometryImpl impl = (GeometryImpl)geometry.PlatformImpl; this.renderTarget.FillGeometry(impl.Geometry, d2dBrush); @@ -122,7 +124,7 @@ namespace Perspex.Direct2D1.Media if (pen != null) { - using (var d2dBrush = this.CreateBrush(pen.Brush)) + using (var d2dBrush = this.CreateBrush(pen.Brush, geometry.GetRenderBounds(pen.Thickness).Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(this.renderTarget)) { GeometryImpl impl = (GeometryImpl)geometry.PlatformImpl; @@ -139,7 +141,7 @@ namespace Perspex.Direct2D1.Media /// The corner radius. public void DrawRectange(Pen pen, Rect rect, float cornerRadius) { - using (var brush = this.CreateBrush(pen.Brush)) + using (var brush = this.CreateBrush(pen.Brush, rect.Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(this.renderTarget)) { this.renderTarget.DrawRoundedRectangle( @@ -162,7 +164,7 @@ namespace Perspex.Direct2D1.Media { var impl = (FormattedTextImpl)text.PlatformImpl; - using (var brush = this.CreateBrush(foreground)) + using (var brush = this.CreateBrush(foreground, impl.Measure())) using (var renderer = new PerspexTextRenderer(this, this.renderTarget, brush)) { impl.TextLayout.Draw(renderer, (float)origin.X, (float)origin.Y); @@ -178,7 +180,7 @@ namespace Perspex.Direct2D1.Media /// The corner radius. public void FillRectange(Perspex.Media.Brush brush, Rect rect, float cornerRadius) { - using (var b = this.CreateBrush(brush)) + using (var b = this.CreateBrush(brush, rect.Size)) { this.renderTarget.FillRoundedRectangle( new RoundedRectangle @@ -263,8 +265,9 @@ namespace Perspex.Direct2D1.Media /// Creates a Direct2D brush from a Perspex brush. /// /// The perspex brush. + /// The size of the brush's target area. /// The Direct2D brush. - public Disposable CreateBrush(Perspex.Media.Brush brush) + public Disposable CreateBrush(Perspex.Media.Brush brush, Size destinationSize) { var solidColorBrush = brush as Perspex.Media.SolidColorBrush; var visualBrush = brush as Perspex.Media.VisualBrush; @@ -278,7 +281,7 @@ namespace Perspex.Direct2D1.Media } else if (visualBrush != null) { - return this.CreateBrush(visualBrush); + return this.CreateBrush(visualBrush, destinationSize); } else { @@ -292,8 +295,9 @@ namespace Perspex.Direct2D1.Media /// Creates a Direct2D from a Perspex . /// /// The perspex brush. + /// The size of the brush's target area. /// The Direct2D brush. - private Disposable CreateBrush(VisualBrush brush) + private Disposable CreateBrush(VisualBrush brush, Size destinationSize) { var visual = brush.Visual; var layoutable = visual as ILayoutable; @@ -304,17 +308,19 @@ namespace Perspex.Direct2D1.Media layoutable.Arrange(new Rect(layoutable.DesiredSize)); } + var sourceSize = layoutable.Bounds.Size; + var destinationRect = brush.DestinationRect.ToPixels(destinationSize); + var scale = brush.Stretch.CalculateScaling(destinationRect.Size, sourceSize); + using (var target = new BitmapRenderTarget( this.renderTarget, CompatibleRenderTargetOptions.None, - visual.Bounds.Size.ToSharpDX())) + destinationRect.Size.ToSharpDX())) { var renderer = new Renderer(target); - renderer.Render(visual, null); + renderer.Render(visual, null, Matrix.Identity, Matrix.CreateScale(scale)); var result = new BitmapBrush(this.renderTarget, target.Bitmap); - result.ExtendModeX = ExtendMode.Wrap; - result.ExtendModeY = ExtendMode.Wrap; return new Disposable(result, target); } } diff --git a/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs b/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs index 5566c2ff60..9319c86328 100644 --- a/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs +++ b/src/Windows/Perspex.Direct2D1/Media/PerspexTextRenderer.cs @@ -49,9 +49,11 @@ namespace Perspex.Direct2D1.Media ComObject clientDrawingEffect) { var wrapper = clientDrawingEffect as BrushWrapper; + + // TODO: Work out how to get the size below rather than passing new Size(). var brush = (wrapper == null) ? this.foreground : - this.context.CreateBrush(wrapper.Brush); + this.context.CreateBrush(wrapper.Brush, new Size()); this.renderTarget.DrawGlyphRun( new Vector2(baselineOriginX, baselineOriginY), diff --git a/tests/Perspex.RenderTests/Brushes/VisualBrushTests.cs b/tests/Perspex.RenderTests/Brushes/VisualBrushTests.cs index 772d3a4917..1cb4c526f3 100644 --- a/tests/Perspex.RenderTests/Brushes/VisualBrushTests.cs +++ b/tests/Perspex.RenderTests/Brushes/VisualBrushTests.cs @@ -20,7 +20,7 @@ namespace Perspex.Direct2D1.RenderTests.Controls } [Fact] - public void VisualBrush_Should_Draw_Visual() + public void VisualBrush_Stretch_None() { Decorator target = new Decorator { @@ -54,5 +54,123 @@ namespace Perspex.Direct2D1.RenderTests.Controls this.RenderToFile(target); this.CompareImages(); } + + [Fact] + public void VisualBrush_Align_Center() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Rectangle + { + Fill = new VisualBrush + { + AlignmentX = AlignmentX.Center, + AlignmentY = AlignmentY.Center, + Visual = new Border + { + Width = 92, + Height = 92, + Background = Brushes.Red, + BorderBrush = Brushes.Black, + BorderThickness = 2, + Child = new TextBlock + { + Text = "Perspex", + FontSize = 12, + FontFamily = "Arial", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + } + } + } + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + + [Fact] + public void VisualBrush_Stretch_Fill_Large() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 920, + Height = 920, + Child = new Rectangle + { + Fill = new VisualBrush + { + Stretch = Stretch.Fill, + Visual = new Border + { + Width = 92, + Height = 92, + Background = Brushes.Red, + BorderBrush = Brushes.Black, + BorderThickness = 2, + Child = new TextBlock + { + Text = "Perspex", + FontSize = 12, + FontFamily = "Arial", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + } + } + } + } + }; + + this.RenderToFile(target); + this.CompareImages(); + } + + ////[Fact] + ////public void VisualBrush_Line_Fill() + ////{ + //// Decorator target = new Decorator + //// { + //// Padding = new Thickness(8), + //// Width = 200, + //// Height = 200, + //// Child = new Line + //// { + //// X1 = 16, + //// Y1 = 16, + //// X2 = 184, + //// Y2 = 184, + //// StrokeThickness = 40, + //// StrokeStartLineCap = "Triangle", + //// StrokeEndLineCap = "Triangle", + //// Fill = new VisualBrush + //// { + //// Visual = new Border + //// { + //// Width = 92, + //// Height = 92, + //// Background = Brushes.Red, + //// BorderBrush = Brushes.Black, + //// BorderThickness = 2, + //// Child = new TextBlock + //// { + //// Text = "Perspex", + //// FontSize = 12, + //// FontFamily = "Arial", + //// HorizontalAlignment = HorizontalAlignment.Center, + //// VerticalAlignment = VerticalAlignment.Center, + //// } + //// } + //// } + //// } + //// }; + + //// this.RenderToFile(target); + //// this.CompareImages(); + ////} } } diff --git a/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj b/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj index 455d7a993c..dbe991d419 100644 --- a/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj +++ b/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj @@ -77,6 +77,7 @@ +