diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml index e78cf2bc22..6e649b39a5 100644 --- a/samples/ControlCatalog/Pages/ViewboxPage.xaml +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml @@ -1,66 +1,35 @@ - - - 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 index 1b5f4bc7f4..7a2bceb8f2 100644 --- a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs @@ -1,18 +1,47 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace ControlCatalog.Pages { public class ViewboxPage : UserControl { + private readonly Viewbox _viewbox; + private readonly ComboBox _stretchSelector; + public ViewboxPage() { - this.InitializeComponent(); + InitializeComponent(); + + _viewbox = this.FindControl("Viewbox"); + + _stretchSelector = this.FindControl("StretchSelector"); + + _stretchSelector.Items = new[] + { + Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None + }; + + _stretchSelector.SelectedIndex = 0; + + var stretchDirectionSelector = this.FindControl("StretchDirectionSelector"); + + stretchDirectionSelector.Items = new[] + { + StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly + }; + + stretchDirectionSelector.SelectedIndex = 0; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void StretchSelector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + _viewbox.Stretch = (Stretch) _stretchSelector.SelectedItem!; + } } } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 247b62d3cf..c448729643 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -31,8 +31,8 @@ namespace Avalonia.Controls static Image() { - AffectsRender(SourceProperty, StretchProperty); - AffectsMeasure(SourceProperty, StretchProperty); + AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); + AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); } /// diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 781c93bcbe..b65f4b31d8 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -1,40 +1,50 @@ -using System; -using Avalonia.Media; +using Avalonia.Media; namespace Avalonia.Controls { /// - /// Viewbox is used to scale single child. + /// Viewbox is used to scale single child to fit in the available space. /// /// public class Viewbox : Decorator { /// - /// The stretch property + /// Defines the property. /// public static readonly AvaloniaProperty StretchProperty = - AvaloniaProperty.RegisterDirect(nameof(Stretch), - v => v.Stretch, (c, v) => c.Stretch = v, Stretch.Uniform); + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchDirectionProperty = + AvaloniaProperty.Register(nameof(StretchDirection), StretchDirection.Both); private Stretch _stretch = Stretch.Uniform; + static Viewbox() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + AffectsMeasure(StretchProperty, StretchDirectionProperty); + } + /// /// 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() + /// + /// Gets or sets a value controlling in what direction contents will be stretched. + /// + public StretchDirection StretchDirection { - ClipToBoundsProperty.OverrideDefaultValue(true); - AffectsMeasure(StretchProperty); + get => GetValue(StretchDirectionProperty); + set => SetValue(StretchDirectionProperty, value); } protected override Size MeasureOverride(Size availableSize) @@ -47,9 +57,9 @@ namespace Avalonia.Controls var childSize = child.DesiredSize; - var scale = GetScale(availableSize, childSize, Stretch); + var size = Stretch.CalculateSize(availableSize, childSize, StretchDirection); - return (childSize * scale).Constrain(availableSize); + return size.Constrain(availableSize); } return new Size(); @@ -62,7 +72,9 @@ namespace Avalonia.Controls if (child != null) { var childSize = child.DesiredSize; - var scale = GetScale(finalSize, childSize, Stretch); + var scale = Stretch.CalculateScaling(finalSize, childSize, StretchDirection); + + // TODO: Viewbox should have another decorator as a child so we won't affect other render transforms. var scaleTransform = child.RenderTransform as ScaleTransform; if (scaleTransform == null) @@ -81,44 +93,5 @@ namespace Avalonia.Controls 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/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index e005bafbf9..7eaec35506 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -114,5 +114,61 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2.0, scaleTransform.ScaleX); Assert.Equal(2.0, scaleTransform.ScaleY); } + + [Theory] + [InlineData(50, 100, 50, 100, 50, 100, 1)] + [InlineData(50, 100, 150, 150, 50, 100, 1)] + [InlineData(50, 100, 25, 50, 25, 50, 0.5)] + public void Viewbox_Should_Return_Correct_SizeAndScale_StretchDirection_DownOnly( + double childWidth, double childHeight, + double viewboxWidth, double viewboxHeight, + double expectedWidth, double expectedHeight, + double expectedScale) + { + var target = new Viewbox + { + Child = new Control { Width = childWidth, Height = childHeight }, + StretchDirection = StretchDirection.DownOnly + }; + + target.Measure(new Size(viewboxWidth, viewboxHeight)); + target.Arrange(new Rect(default, target.DesiredSize)); + + Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); + + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(expectedScale, scaleTransform.ScaleX); + Assert.Equal(expectedScale, scaleTransform.ScaleY); + } + + [Theory] + [InlineData(50, 100, 50, 100, 50, 100, 1)] + [InlineData(50, 100, 25, 50, 25, 50, 1)] + [InlineData(50, 100, 150, 150, 75, 150, 1.5)] + public void Viewbox_Should_Return_Correct_SizeAndScale_StretchDirection_UpOnly( + double childWidth, double childHeight, + double viewboxWidth, double viewboxHeight, + double expectedWidth, double expectedHeight, + double expectedScale) + { + var target = new Viewbox + { + Child = new Control { Width = childWidth, Height = childHeight }, + StretchDirection = StretchDirection.UpOnly + }; + + target.Measure(new Size(viewboxWidth, viewboxHeight)); + target.Arrange(new Rect(default, target.DesiredSize)); + + Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); + + var scaleTransform = target.Child.RenderTransform as ScaleTransform; + + Assert.NotNull(scaleTransform); + Assert.Equal(expectedScale, scaleTransform.ScaleX); + Assert.Equal(expectedScale, scaleTransform.ScaleY); + } } }