Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial ConstrainedBox control for AspectRatio and Scaling #4104

Merged

Conversation

michael-hawker
Copy link
Member

@michael-hawker michael-hawker commented Jul 9, 2021

Fixes #4103

Adds the new ConstrainedPresenter control which can be used around an element to constrain it's layout based on a scale and/or aspect ratio. Adds an AspectRatio helper as well so you can just type in 16:9 as an aspect ratio as well.

PR Type

What kind of change does this PR introduce?

  • Feature

What is the new behavior?

The control will first scale the available size by the ScaleX and ScaleY properties (default to 1.0) and then constrain that space to the desired AspectRatio size.

TODO:

  • Can't use x:Bind to bind to Double (can we use an implicit type conversion on AspectRatio?):

    image

  • Investigate 1px rounding error on edges (click image to zoom and see the light-gray strip on left side):

    image

  • Unit Tests

  • Add bounds check to Scale properties, probably want to ensure AspectRatio is non-zero and positive as well.

  • Try out more scenarios

  • Better sample

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Pull Request has been submitted to the documentation repository instructions. Link:
  • Sample in sample app has been added / updated (for bug fixes / features)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Header has been added to all new source files (run build/UpdateHeaders.bat)
  • Contains NO breaking changes

Other information

FYI @chingucoding @JustinXinLiu if you want to give it a whirl.

@ghost
Copy link

ghost commented Jul 9, 2021

Thanks michael-hawker for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌

@ghost ghost requested review from azchohfi and Rosuavio July 9, 2021 23:58
@ghost ghost added feature request 📬 A request for new changes to improve functionality sample app 🖼 labels Jul 9, 2021
}

/// <summary>
/// Gets or sets the scale for the width of the panel. Should be a value between 0-1.0. Default is 1.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if it is not between 0 and 1? What if it is negative? I think both cases would cause issues with the layout-system, especially if the scale value is negative.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, could probably coerce the value, would be nice if UWP XAML had a better system for that. Could at least clip to the bounds in the setter though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that would be a good idea. That's also what NumberBox does.


private Size CalculateConstrainedSize(Size initialSize)
{
var availableSize = new Size(initialSize.Width * ScaleX, initialSize.Height * ScaleY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: What if the content inside says "I want to be 100px wide at most"? According to docs, since we are more or less acting like a panel, we should respect that. However we are not unless base.MeasureOverride/base.ArrangeOverride does that. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure the base calls should follow those constraints, the sample I had kind of tests that scenario, but we could test some others to be sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe as part of some automated UI tests but should be fine then. Just a bit unsettling not knowing what exactly happens on the base calls.

}

/// <summary>
/// Gets or sets the scale for the width of the panel. Should be a value between 0-1.0. Default is 1.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that would be a good idea. That's also what NumberBox does.


private Size CalculateConstrainedSize(Size initialSize)
{
var availableSize = new Size(initialSize.Width * ScaleX, initialSize.Height * ScaleY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe as part of some automated UI tests but should be fine then. Just a bit unsettling not knowing what exactly happens on the base calls.

@@ -28,7 +28,7 @@ public Case CurrentCase
}

/// <summary>
/// Indicates the <see cref="CurrentCase"/> property.
/// Identifies the <see cref="CurrentCase"/> property.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could argue this is not part of the ContrainedPresenter :P

Comment on lines 10 to 12
<controls:ConstrainedPresenter AspectRatio="16:3" VerticalAlignment="Top">
<Image Source="/Assets/Photos/WestSeattleView.jpg" Stretch="UniformToFill"/>
</controls:ConstrainedPresenter>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a WIP ignore the comment below.

I think if we truly want to showcase the controls potential, we have a "root" panel that you can change in size and inside the ContrainedPresenter have a control that does not preserve aspect ratio on it's own. Maybe an ellipse with alignments set to stretch so that you can showcase how it maintains the aspect ratio.

@michael-hawker
Copy link
Member Author

Can't use x:Bind on a double field to the AspectRatio type, should try to see if I can use an implicit conversion overload on the class to get that to work?

@marcelwgn
Copy link
Contributor

Can't use x:Bind on a double field to the AspectRatio type, should try to see if I can use an implicit conversion overload on the class to get that to work?

With what error is it failing? Could the property not being a DP be an issue there?

@michael-hawker
Copy link
Member Author

@chingucoding had to add implicit operator conversions: see commit 4fe6c67.

Hit some issues trying to inherit directly from FrameworkElement, which is done in WinUI, but not as apparent in C#, see doc issue here I added a comment to: MicrosoftDocs/winrt-api#646

Did find that ParallaxView does handle the alignment of the child, so that'll be something extra we need to do being lower-level: https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/ParallaxView/ParallaxView.cpp#L117 - Beyond that though seems hopefully straight-forward once we know the "magic"?

@marcelwgn
Copy link
Contributor

@chingucoding had to add implicit operator conversions: see commit 4fe6c67.

Hit some issues trying to inherit directly from FrameworkElement, which is done in WinUI, but not as apparent in C#, see doc issue here I added a comment to: MicrosoftDocs/winrt-api#646

Did find that ParallaxView does handle the alignment of the child, so that'll be something extra we need to do being lower-level: https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/ParallaxView/ParallaxView.cpp#L117 - Beyond that though seems hopefully straight-forward once we know the "magic"?

Hmm yes, my fear is that it might not be as easy as we think and there might be complexity hidden underneath that. But I would say it's worth a shot. If that fails, we could put a ContentPresenter inside the control and let that control handle the alignments in case we have no idea other idea.

@michael-hawker michael-hawker changed the title Add initial ConstrainedPresenter control for AspectRatio and Scaling Add initial ConstrainedBox control for AspectRatio and Scaling Jul 23, 2021
@michael-hawker
Copy link
Member Author

Extensions.LogicalTree.cs(212,14): error SA1028: Code should not contain trailing whitespace

…ample.

TODO: Tests and trying out more scenarios and better sample.
… mathmatical expressions and double can be bound from x:Bind directly!
1px Rounding was caused by multiple unnecessary passes of Arrange, so only re-calculated if needed (when parent Panel doesn't respect Measure request i.e. Grid Stretch)
Also adds in handling for Infinity scenarios like StackPanel/ScrollViewer and can try and respect child measurements
Adds initial basic tests as a starting point for more tests
@michael-hawker michael-hawker force-pushed the constrained-presenter branch from 1a68b20 to 0fd2a37 Compare July 29, 2021 02:51
@michael-hawker
Copy link
Member Author

Alright, fixed a lot of issues with the implementation to handle the 1px rounding issue as well as panels that provide Infinity for availableSize. We should cover a lot of scenarios now.

Started adding some unit tests, will add more next to check other scenarios. @chingucoding adding extra constraints should be handled, but I'll write some tests to be sure for Min/Width/Max etc...

@sylveon
Copy link

sylveon commented Jul 29, 2021

I have a use-case where I'd like to constrain a control's width/height to multiples of 4, irrespective of the control's actual aspect ratio. Is it possible with this control?

@michael-hawker
Copy link
Member Author

I have a use-case where I'd like to constrain a control's width/height to multiples of 4, irrespective of the control's actual aspect ratio. Is it possible with this control?

Not currently, but I could see us adding a MultipleX and MultipleY property to add that behavior (other name suggestions are welcome). Figured you may want them independent. Just trying to think of order of operations for how applying all the pieces together would work.

I think Scale always occurs first. Then the question is if you apply the Aspect Ratio next and snap to the closest values (which means your Aspect Ratio would be slightly off) or if you'd snap the remaining area next and then apply the AspectRatio (which could mean your snap is off)...

We could theoretically have another setting that defines the order of operations, but then that may be a bit much... I think having these types of properties on one panel is useful, some of them are useful combined, but then I don't know if using them all together or in certain combinations is always useful...

Thoughts?

@michael-hawker
Copy link
Member Author

Had thought of something like this:

<controls:ContrainedBox VerticalAlignment="Top">
  <controls:ConstrainedBox.Constraints>
    <controls:ScaleConstraint ScaleX="0.9" ScaleY="0.9"/>
    <controls:AspectRatioConstraint AspectRatio="16:9"/>
    <controls:MultipleConstraint OfX="4" OfY="4"/>
  </controls:ConstrainedBox.Constraints>
  <Image Source="/Assets/MyImage.png" Stretch="UniformToFill"/>
</controls:ConstrainedBox>

With ability maybe to pull them out as a resource: <controls:ConstrainedBox Constraints="{StaticResource MyConstraints}">.

That adds a lot of complexity though and verbosity. So think just going to add the Multiple properties; order will be Scale, Multiples, then AspectRatio. We'll see if we need configuration or more generalization in the future.

@michael-hawker michael-hawker added the next preview ✈️ Label for marking what we want to include in the next preview release for developers to try. label Jul 29, 2021
@michael-hawker
Copy link
Member Author

I added a checkerboard pattern to the sample to test the Multiple feature; however, this exposed some shortcomings in the TilesBrush and the SurfaceLoader for creating a DPI-aware tiled brush that could be aligned to its container's layout...

I'd like to use this scenario to test the layout, but at the same time, it's extra code in other areas of the project. I may split that out as a separate PR for the future and just go back to some tests to check the layout numbers instead of making it look pretty first...

I'll open a new issue for tracking: #4150

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json Outdated Show resolved Hide resolved
Comment on lines +60 to +73
/// <summary>
/// Identifies the <see cref="MultipleX"/> property.
/// </summary>
public static readonly DependencyProperty MultipleXProperty =
DependencyProperty.Register(nameof(MultipleX), typeof(int), typeof(ConstrainedBox), new PropertyMetadata(null));

/// <summary>
/// Gets or sets the integer multiple that the height of the panel should be floored to. Default is null (no snap).
/// </summary>
public int MultipleY
{
get { return (int)GetValue(MultipleYProperty); }
set { SetValue(MultipleYProperty, value); }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be an integer? What if I want the value to be a multiple of 3.5.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose, but why would you want to snap to an un-even number of pixels?

/// The <see cref="ConstrainedBox"/> is a <see cref="FrameworkElement"/> control akin to <see cref="Viewbox"/>
/// which can modify the behavior of it's child element's layout. <see cref="ConstrainedBox"/> restricts the
/// available size for its content based on a scale factor, multiple factor, and/or a specific <see cref="AspectRatio"/>, in that order.
/// This is performed as a layout calculation modification.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rephrase this to make it more clear that this is the key difference compared to ViewBox?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what the rest of the paragraph goes to describe? What would you clarify?

Co-authored-by: Marcel Wagner <marcel.alex.wagner@outlook.com>
@ghost
Copy link

ghost commented Aug 2, 2021

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

@michael-hawker michael-hawker marked this pull request as ready for review August 2, 2021 19:07
@michael-hawker
Copy link
Member Author

Alright think we're in a good enough state for an initial preview. Can add some more tests and samples later (want to fix #4150 as well after preview).

@RosarioPulella @XAML-Knight thoughts on MultipleX/Y being double over int?

@XAML-Knight
Copy link
Contributor

...thoughts on MultipleX/Y being double over int?

While a double value would offer greater precision, an int in this case may be conceptually easier to grasp. I'd suggest leaving it as int, with the option to convert to another data type, if we find we need to.

@ghost ghost removed the needs attention 👋 label Aug 2, 2021
@Rosuavio
Copy link
Contributor

Rosuavio commented Aug 3, 2021

@RosarioPulella @XAML-Knight thoughts on MultipleX/Y being double over int?

I agree with @XAML-Knight, double feels like premature optimization.

As a side note, I actually think the aspect ratio should only ever be represented as a pair of ints, as they ints are much less fuzzy in there possible values and two ints are enough to represent any aspect ratio. In that regard I don't think the aspect ratio should ever be construable with a pair of doubles, a single double, or represented as the double from the width / height. I think we should take a late binding strategy and do any math with the width and the height when needed rather than provide an implicit conversion to double, to avoid any confusion in the future.

@michael-hawker
Copy link
Member Author

Thanks for the note @RosarioPulella, as discussed. While I think it makes sense, it does seem there are a couple of more common Aspect Ratios that are irrational and can't be represented as easily with just two int values. Let's leave it as is for now. 🙂

Copy link
Contributor

@Rosuavio Rosuavio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it seems I made an assumption based on how I have seen aspect ratios in the wild. Doubles for the aspect ratio's width and the height could be necessary, not just a good idea.

As for the Value property and the implicit conversion so a single double, I still think its better to simply do that math explicitly, but I don't think its that bad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
controls 🎛️ feature 💡 feature request 📬 A request for new changes to improve functionality next preview ✈️ Label for marking what we want to include in the next preview release for developers to try. sample app 🖼
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Aspect ratio respecting presenter
5 participants