diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 06f808b726..7dd8098300 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -27,5 +27,6 @@
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml
new file mode 100644
index 0000000000..89a82c4791
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml
@@ -0,0 +1,65 @@
+
+
+
+ F1 M 16.6309,18.6563C 17.1309,
+ 8.15625 29.8809,14.1563 29.8809,
+ 14.1563C 30.8809,11.1563 34.1308,
+ 11.4063 34.1308,11.4063C 33.5,12
+ 34.6309,13.1563 34.6309,13.1563C
+ 32.1309,13.1562 31.1309,14.9062
+ 31.1309,14.9062C 41.1309,23.9062
+ 32.6309,27.9063 32.6309,27.9062C
+ 24.6309,24.9063 21.1309,22.1562
+ 16.6309,18.6563 Z M 16.6309,19.9063C
+ 21.6309,24.1563 25.1309,26.1562
+ 31.6309,28.6562C 31.6309,28.6562
+ 26.3809,39.1562 18.3809,36.1563C
+ 18.3809,36.1563 18,38 16.3809,36.9063C
+ 15,36 16.3809,34.9063 16.3809,34.9063C
+ 16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z
+
+
+
+
+
+ Viewbox
+ A control used to scale single child.
+
+
+ None
+ Fill
+ Uniform
+ UniformToFill
+
+
+ Hello World!
+
+
+ Hello World!
+
+
+ Hello World!
+
+
+ Hello World!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
new file mode 100644
index 0000000000..1b5f4bc7f4
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class ViewboxPage : UserControl
+ {
+ public ViewboxPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 87e3853643..950d4f34da 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -5,28 +5,30 @@
// http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs
//
-using Avalonia.Controls.Primitives;
-using Avalonia.Media;
-using Avalonia.VisualTree;
using System;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Reactive.Linq;
+using Avalonia.Media;
namespace Avalonia.Controls
{
///
/// Control that implements support for transformations as if applied by LayoutTransform.
///
- public class LayoutTransformControl : ContentControl
+ public class LayoutTransformControl : Decorator
{
public static readonly AvaloniaProperty LayoutTransformProperty =
AvaloniaProperty.Register(nameof(LayoutTransform));
static LayoutTransformControl()
{
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+
LayoutTransformProperty.Changed
.AddClassHandler(x => x.OnLayoutTransformChanged);
+
+ ChildProperty.Changed
+ .AddClassHandler(x => x.OnChildChanged);
}
///
@@ -38,8 +40,7 @@ namespace Avalonia.Controls
set { SetValue(LayoutTransformProperty, value); }
}
- public Control TransformRoot => _transformRoot ??
- (_transformRoot = this.GetVisualChildren().OfType().FirstOrDefault());
+ public IControl TransformRoot => Child;
///
/// Provides the behavior for the "Arrange" pass of layout.
@@ -132,16 +133,8 @@ namespace Avalonia.Controls
return transformedDesiredSize;
}
- ///
- /// Builds the visual tree for the LayoutTransformerControl when a new
- /// template is applied.
- ///
- protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ private void OnChildChanged(AvaloniaPropertyChangedEventArgs e)
{
- base.OnTemplateApplied(e);
-
- _matrixTransform = new MatrixTransform();
-
if (null != TransformRoot)
{
TransformRoot.RenderTransform = _matrixTransform;
@@ -169,14 +162,14 @@ namespace Avalonia.Controls
///
/// RenderTransform/MatrixTransform applied to TransformRoot.
///
- private MatrixTransform _matrixTransform;
+ private MatrixTransform _matrixTransform = new MatrixTransform();
///
/// Transformation matrix corresponding to _matrixTransform.
///
private Matrix _transformation;
private IDisposable _transformChangedEvent = null;
- private Control _transformRoot;
+
///
/// Returns true if Size a is smaller than Size b in either dimension.
///
@@ -215,7 +208,8 @@ namespace Avalonia.Controls
///
private void ApplyLayoutTransform()
{
- if (LayoutTransform == null) return;
+ if (LayoutTransform == null)
+ return;
// Get the transform matrix and apply it
_transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound);
@@ -376,11 +370,8 @@ namespace Avalonia.Controls
{
var newTransform = e.NewValue as Transform;
- if (_transformChangedEvent != null)
- {
- _transformChangedEvent.Dispose();
- _transformChangedEvent = null;
- }
+ _transformChangedEvent?.Dispose();
+ _transformChangedEvent = null;
if (newTransform != null)
{
@@ -392,4 +383,4 @@ namespace Avalonia.Controls
ApplyLayoutTransform();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs
new file mode 100644
index 0000000000..db753f4ab4
--- /dev/null
+++ b/src/Avalonia.Controls/Viewbox.cs
@@ -0,0 +1,124 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Viewbox is used to scale single child.
+ ///
+ ///
+ public class Viewbox : Decorator
+ {
+ ///
+ /// The stretch property
+ ///
+ public static AvaloniaProperty StretchProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Stretch),
+ v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform);
+
+ private Stretch _stretch = Stretch.Uniform;
+
+ ///
+ /// Gets or sets the stretch mode,
+ /// which determines how child fits into the available space.
+ ///
+ ///
+ /// The stretch.
+ ///
+ public Stretch Stretch
+ {
+ get => _stretch;
+ set => SetAndRaise(StretchProperty, ref _stretch, value);
+ }
+
+ static Viewbox()
+ {
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+ AffectsMeasure(StretchProperty);
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var child = Child;
+
+ if (child != null)
+ {
+ child.Measure(Size.Infinity);
+
+ var childSize = child.DesiredSize;
+
+ var scale = GetScale(availableSize, childSize, Stretch);
+
+ return childSize * scale;
+ }
+
+ return new Size();
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var child = Child;
+
+ if (child != null)
+ {
+ var childSize = child.DesiredSize;
+ var scale = GetScale(finalSize, childSize, Stretch);
+ var scaleTransform = child.RenderTransform as ScaleTransform;
+
+ if (scaleTransform == null)
+ {
+ child.RenderTransform = scaleTransform = new ScaleTransform(scale.X, scale.Y);
+ child.RenderTransformOrigin = RelativePoint.TopLeft;
+ }
+
+ scaleTransform.ScaleX = scale.X;
+ scaleTransform.ScaleY = scale.Y;
+
+ child.Arrange(new Rect(childSize));
+
+ return childSize * scale;
+ }
+
+ return new Size();
+ }
+
+ private static Vector GetScale(Size availableSize, Size childSize, Stretch stretch)
+ {
+ double scaleX = 1.0;
+ double scaleY = 1.0;
+
+ bool validWidth = !double.IsPositiveInfinity(availableSize.Width);
+ bool validHeight = !double.IsPositiveInfinity(availableSize.Height);
+
+ if (stretch != Stretch.None && (validWidth || validHeight))
+ {
+ scaleX = childSize.Width <= 0.0 ? 0.0 : availableSize.Width / childSize.Width;
+ scaleY = childSize.Height <= 0.0 ? 0.0 : availableSize.Height / childSize.Height;
+
+ if (!validWidth)
+ {
+ scaleX = scaleY;
+ }
+ else if (!validHeight)
+ {
+ scaleY = scaleX;
+ }
+ else
+ {
+ 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/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index f45f48c260..48e5f231d0 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -12,7 +12,6 @@
-
diff --git a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml b/src/Avalonia.Themes.Default/LayoutTransformControl.xaml
deleted file mode 100644
index b26f053622..0000000000
--- a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml
+++ /dev/null
@@ -1,13 +0,0 @@
-
\ No newline at end of file
diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
index d5f9818f89..13c946b549 100644
--- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
@@ -1,6 +1,4 @@
-using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
-using Avalonia.Controls.Templates;
using Avalonia.Media;
using Xunit;
@@ -311,20 +309,10 @@ namespace Avalonia.Controls.UnitTests
{
var lt = new LayoutTransformControl()
{
- LayoutTransform = transform,
- Template = new FuncControlTemplate(
- p => new ContentPresenter() { Content = p.Content })
+ LayoutTransform = transform
};
- lt.Content = new Rectangle() { Width = width, Height = height };
-
- lt.ApplyTemplate();
-
- //we need to force create visual child
- //so the measure after is correct
- (lt.Presenter as ContentPresenter).UpdateChild();
-
- Assert.NotNull(lt.Presenter?.Child);
+ lt.Child = new Rectangle() { Width = width, Height = height };
lt.Measure(Size.Infinity);
lt.Arrange(new Rect(lt.DesiredSize));
@@ -332,4 +320,4 @@ namespace Avalonia.Controls.UnitTests
return lt;
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
new file mode 100644
index 0000000000..ad0f318d2f
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
@@ -0,0 +1,105 @@
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class ViewboxTests
+ {
+ [Fact]
+ public void Viewbox_Stretch_Uniform_Child()
+ {
+ var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(200, 200));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(200, 100), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(2.0, scaleTransform.ScaleX);
+ Assert.Equal(2.0, scaleTransform.ScaleY);
+ }
+
+ [Fact]
+ public void Viewbox_Stretch_None_Child()
+ {
+ var target = new Viewbox() { Stretch = Stretch.None, Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(200, 200));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(100, 50), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(1.0, scaleTransform.ScaleX);
+ Assert.Equal(1.0, scaleTransform.ScaleY);
+ }
+
+ [Fact]
+ public void Viewbox_Stretch_Fill_Child()
+ {
+ var target = new Viewbox() { Stretch = Stretch.Fill, Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(200, 200));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(200, 200), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(2.0, scaleTransform.ScaleX);
+ Assert.Equal(4.0, scaleTransform.ScaleY);
+ }
+
+ [Fact]
+ public void Viewbox_Stretch_UniformToFill_Child()
+ {
+ var target = new Viewbox() { Stretch = Stretch.UniformToFill, Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(200, 200));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(200, 200), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(4.0, scaleTransform.ScaleX);
+ Assert.Equal(4.0, scaleTransform.ScaleY);
+ }
+
+ [Fact]
+ public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Width()
+ {
+ var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(double.PositiveInfinity, 200));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(400, 200), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(4.0, scaleTransform.ScaleX);
+ Assert.Equal(4.0, scaleTransform.ScaleY);
+ }
+
+ [Fact]
+ public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Height()
+ {
+ var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } };
+
+ target.Measure(new Size(200, double.PositiveInfinity));
+ target.Arrange(new Rect(new Point(0, 0), target.DesiredSize));
+
+ Assert.Equal(new Size(200, 100), target.DesiredSize);
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(2.0, scaleTransform.ScaleX);
+ Assert.Equal(2.0, scaleTransform.ScaleY);
+ }
+ }
+}