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); + } + } +}