react-native-windows
react-native-windows copied to clipboard
Add method to invoke nested Yoga layout
Description
Type of Change
Erase all that don't apply.
- New feature (non-breaking change which adds functionality)
Why
Yoga does not recurse layout beyond a node that provides a self-measurement function. If you want to build a custom control that allows for XAML layout with descendants from React that still use Yoga layout, you need to kick off a new layout calculation from each "inner root".
Resolves #7601
What
This change adds an API to XamlUIService to kick off Yoga layout from any React tag, allowing custom controls to, for example, override MeasureOverride in a "self-measuring" XAML component and invoke XamlUIService::LayoutElement to trigger the nested Yoga calculation.
The main limitation with this approach is that XAML is likely to draw the layout changes in two separate passes, the first pass draws from the React root to the self-measuring view, and because the Width/Height/Top/Bottom properties get set for the "inner root" and it's descendants, these will require a separate layout and draw pass by XAML to commit.
Microsoft Reviewers: Open in CodeFlow
I think in order to use this API, your native VM must implement a panel so that you can override layout, is that right? can you provide a sample of how this API is to be used? Ideally if you just want to have a native VM for e.g. a XAML Grid, you shouldn't need to declare a subclass of Grid just so that you can override measure/arrange
@asklar - indeed - there's not really any alternative for this. I'll work up a sample at some point.
We're considering other options for handling re-entrant / nested layout. Converting to draft for now.
Note: dependent on #10422, leaving in draft state until #10422 is merged
Please note, this change is also going to be useful should we ever support nested Views in Text with RichTextBlock, as we'll need an option to invoke Yoga layout on a specific sub-tree from TextViewManager.
@asklar - this is the best example I can think of without adding a custom VM to playground. We don't have any such examples yet, so not planning on setting that up for this PR. The sequence of events would look something like the following to implement nested Views in Text.
- View added as descendant of Text in React JS
- TextViewManager or VirtualTextViewManager detects child in not Inline but some type of UIElement
- Node where child is added notifies TextViewManager that it needs to replace the native view with RichTextBlock
- View is added to RichTextBlock via InlineUIContainer
- On subsequent
SetLayoutPropscall, state in TextViewManager shadow node knows that we need to recursively invoke Yoga for the nested Views - Nodes in Text inline tree are visited, when we hit an InlineUIContainer, we invoke
ApplyYogaLayouton NativeUIManager.
Another good thing to note is that this type of thing is already supported on the public surface for Android and iOS view managers / shadow nodes, so strictly speaking it's a bug / gap that we don't support this in RNW.
Based on conversation with @asklar - moving to draft to confirm this does not break rn-xaml.
This is a net new API, should not impact react-native-xaml. Lifting out of draft. Please note, this API is mostly useless without #10761.
ping @chrisglein
Change makes sense to me in that your handing off the responsibilities to the ViewManager to deal with this whole business. The one code example you have here just has one child and calls ApplyLayout manually on them. What has this looked like with your more complicated ViewManagers? (I'm assuming you've applied this code to Messenger) What would it look like to implement in codegen like react-native-xaml?
I'm prone to accept this change, but want to set up the right guidance for folks to write CalculateLayoutOnChildren correctly. What would the documentation for that look like? With that documented, this change looks good.
What has this looked like with your more complicated ViewManagers? (I'm assuming you've applied this code to Messenger)
In the Messenger example, it's a bit simpler. It doesn't implicate XAML layout at all. There's a set of self-measuring leaf nodes in our call window example, where the grid layout for the call window is some cross-platform C++ algorithm. Each of these self-measuring grids has nested React content in it, so we use this to allow re-entrancy for Yoga layout.
What would it look like to implement in codegen like react-native-xaml?
It's a good question. I would think that in react-native-xaml, we might do something like the following:
- Add a custom DependencyProperty to every component generated by react-native-xaml
- For every "parent" component in react-native-xaml, we codegen the CalculateLayoutOnChildren interface / method
- Inside that method, we check each child for the custom DependencyProperty
- If the custom DependencyProperty is present, we take no action, we let XAML "do it's thing"
- If the custom DependencyProperty is not present, we perform this re-entry layout by waiting for something like the SizeChanging event and calling ApplyLayout. This of course is not ideal, because SizeChanging fires out of band from the layout pass, so the children would get updated in a subsequent layout pass, causing possible tearing. I guess the alternative is that every react-native-xaml component gets some kind of derived XAML component type, and we invoke ApplyLayout in ArrangeOverride / MeasureOverride to keep it in a single layout pass.
This breaks down pretty quickly if anyone wanted to add a module that interoperates with react-native-xaml though. So it might be better for this custom DependencyProperty to be part of React Native, and get automatically assigned to any IViewManagerRequiresNativeLayout ABIViewManager component.
Alternatively, we could do this on behalf of all possible XAML layout interoperable modules (via SizeChanged + ApplyLayout) so react-native-xaml gets this "for free" at the expense of tearing.
The reason we went with the more flexible approach of asking the user to invoke ApplyLayout directly was so that we could decide the layout constraints passed to Yoga, rather than assuming that all available width/height gets passed to Yoga (which we would need to do if we wanted to create a baseline functionality for this). It's not a deal breaker, but one option is certainly more "flexible" than the other (at the expense of more code).
What would the documentation for that look like?
It's a good question. I don't think any of the IViewManager interfaces are very well documented today. Our internal documentation is basically "go look at some other module that is already using the interface" :(
This is the closest thing you have today: https://microsoft.github.io/react-native-windows/docs/view-managers This needs to be broken down by interface, giving an example of how it's implemented and what it's useful for.
Also, IMO, there's no better way to document something than to give an example of it's use, but discoverability is certainly an issue.
@chrisglein The more important consideration here though is how this will work with Fabric. React Native Windows already has a nice abstraction for this (i.e., LayoutService) where we can route calls to either Paper or Fabric depending on which UIManager is managing the node. So, LayoutService::ApplyLayout could still work with Fabric, but we'll need to eventually update LayoutService to trigger Yoga layout on Fabric node sub-trees.
@chrisglein can you follow up on @rozele 's answers and approve if this is ready?
This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within 7 days of this comment.