-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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 support for display: contents
#46584
base: main
Are you sure you want to change the base?
Add support for display: contents
#46584
Conversation
void TextInputShadowNode::initialize() { | ||
if (yogaNode_.style().display() == yoga::Display::Contents) { | ||
traits_.set(ShadowNodeTraits::Trait::Hidden); | ||
} else { | ||
traits_.unset(ShadowNodeTraits::Trait::Hidden); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same approach wasn't working on Android, I assume that it may be related to this
react-native/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
Lines 217 to 223 in 151008f
#ifndef ANDROID | |
// T153547836: Disabled on Android because the mounting infrastructure | |
// is not fully ready yet. | |
if (childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { | |
continue; | |
} | |
#endif |
yogaNode_.setChildren({}); | ||
} | ||
|
||
if (fragment.children || (!isDisplayContents && wasDisplayContents)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the node had display: contents
but doesn't anymore, its children need to be updated since the list wasn't initialized and they were mounted higher up the tree.
// TODO: Why does this not use `ShadowNodeFragment::statePlaceholder()` like | ||
// `adoptYogaChild()`? | ||
clonedChildNode = childNode.clone( | ||
{ShadowNodeFragment::propsPlaceholder(), | ||
ShadowNodeFragment::childrenPlaceholder(), | ||
childNode.getState()}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really related, but I think the answer is related to #44796. TL;DR cloning a node with a state placeholder will replace the state in the node with the most recently committed one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for all of the perseverance here 😅. Still trying to wrap my head around all the changes here.
The bookkeeping here around mutation and adding/removing the prop seems to add more a complexity than I had hoped. Do you think this would be any simpler if we went the route of properly teaching Yoga about display: contents
? RN would still need to have knowledge of it, but we get to avoid some of the tricky scenarios here (but need to teach Yoga during layout traversal).
@@ -38,7 +38,8 @@ YG_ENUM_DECL( | |||
YG_ENUM_DECL( | |||
YGDisplay, | |||
YGDisplayFlex, | |||
YGDisplayNone) | |||
YGDisplayNone, | |||
YGDisplayContents) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure what's the best way to fully separate Display::Contents from Yoga since the two places where display property is stored are the yoga node and props which hold yoga styles
The Props structure holding a yoga::Style
is something I plan to change in the future more broadly. These should ideally not be coupled together.
We have some extra props on YogaStylableProps not part of yoga::Style
that we flatten when transferring props to Yoga node. Maybe we could back display prop by field, and ignore the one on the Yoga style for now during props part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this close to what you had in mind?
if (yogaNode_.style().display() == yoga::Display::Contents) { | ||
yogaNode_.style().setDisplay(yoga::Display::None); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you link to spec about why we do this? Though, I'm not sure if it is safe to mutate Yoga node at this point. Maybe makes sense to have some sort of trait to control this which is read when we are transferring props to Yoga node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://drafts.csswg.org/css-display/#unbox-html
I assumed that TextInput
would correspond to input
.
if (viewProps.yogaStyle.display() == yoga::Display::Contents) { | ||
formsStackingContext = false; | ||
formsView = false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ChildrenFormStackingContext could still cause the view to be materialized here. Also think this logic may not run for non-view-derived but still container components. Think we might need to expose this to Differentiator in different way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right. I've moved the logic to Differentiator and added a new trait that allows a view to be force-flattened in 624969b. What do you think about this approach?
@@ -149,12 +139,33 @@ YogaLayoutableShadowNode::YogaLayoutableShadowNode( | |||
static_cast<const YogaLayoutableShadowNode&>(sourceShadowNode) | |||
.yogaTreeHasBeenConfigured_; | |||
} | |||
|
|||
bool wasDisplayContents = hasDisplayContentsStyle(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we derive this from the source shadow node, instead of the temp state of the Yoga node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's obvious now that you've pointed it out, done in bc1984f
if (!getTraits().check(ShadowNodeTraits::Trait::LeafYogaNode) && !isDisplayContents) { | ||
for (auto& child : getChildren()) { | ||
if (auto layoutableChild = std::dynamic_pointer_cast<const YogaLayoutableShadowNode>(child)) { | ||
runForEveryConcreteSubtree(layoutableChild, [&](const YogaLayoutableShadowNode::Shared subtreeRoot) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runForEveryConcreteSubtree(layoutableChild, [&](const YogaLayoutableShadowNode::Shared subtreeRoot) { | |
runForEveryConcreteSubtree(layoutableChild, [&](const YogaLayoutableShadowNode::Shared& subtreeRoot) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in bc1984f
bool isDisplayContents = hasDisplayContentsStyle(); | ||
|
||
if (!getTraits().check(ShadowNodeTraits::Trait::LeafYogaNode) && !isDisplayContents) { | ||
for (auto& child : getChildren()) { | ||
if (auto layoutableChild = std::dynamic_pointer_cast<const YogaLayoutableShadowNode>(child)) { | ||
runForEveryConcreteSubtree(layoutableChild, [&](const YogaLayoutableShadowNode::Shared subtreeRoot) { | ||
yogaLayoutableChildren_.push_back({ | ||
.directChild = layoutableChild, | ||
.yogaChild = subtreeRoot, | ||
}); | ||
}); | ||
} | ||
} | ||
} | ||
|
||
if (isDisplayContents) { | ||
yogaNode_.setChildren({}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we fold this logic into updateYogaChildren
? Perhaps passing previous shadow node, if we need to know before/after?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can but we would need to call it every time instead of when a fragment contains new children. Without either the current logic or updating updateYogaChildren
the list would be empty for clones of the node.
After looking at it closer though I realized that I could remove iterating over subrees from there as it always happens before the method is called (c14b801)
if (hasDisplayContentsStyle()) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like a logic error if we try to adopt here. Could we assert instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also a nit, we should add this after the preconditions below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/* | ||
* Describes a shadow node which is a direct child in the Yoga tree, | ||
* not necessarily in the shadow tree. In most cases directChild == yogaChild, | ||
* the exception being when directChild has its display set to `contents`. | ||
* In that case yogaChild points to a ShadowNode whose yogaNode is a direct | ||
* child of this one's. | ||
*/ | ||
struct ChildPair { | ||
Shared directChild; | ||
Shared yogaChild; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you remind me why we need to store this state, vs deriving it during iteration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way there's no need to check whether a child has display: contents
set on it before doing anything on an item from the list. Otherwise, it would need to be checked first whether the node from the list is a direct child or not. In some cases, where access to the direct child is required (when replacing children), the direct child would need to be found.
Having access to both allows to just directly refer to the node that's needed at the time.
void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { | ||
// Reading data from a dirtied node does not make sense. | ||
react_native_assert(!yogaNode_.isDirty()); | ||
|
||
for (auto childYogaNode : yogaNode_.getChildren()) { | ||
for (auto child : yogaLayoutableChildren_) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: don't copy shared_ptr by value for each iteration here (previous version was copying raw ptr)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in bc1984f
for (const auto &child : getChildren()) { | ||
if (const auto& layoutableChild = std::dynamic_pointer_cast<const YogaLayoutableShadowNode>(child)) { | ||
if (layoutableChild->hasDisplayContentsStyle()) { | ||
auto& contentsNode = shadowNodeFromContext(&layoutableChild->yogaNode_); | ||
contentsNode.setContentsLayoutMetrics(layoutContext); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to do this if we still iterate through children in layout()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout info is only propagated for nodes that are present in the Yoga tree. Since nodes with display: contents
are not present there, they were effectively skipped.
I only have a high-level idea of how Yoga works but I imagine the approach would be relatively similar - instead of removing the nodes from the Yoga tree like in this PR, we would need to skip them during layout. Though, it's hard to say where it fits on the |
Seems like the approach with setting I've also started seeing crashes immediately on startup on Android but this may be a local issue as it doesn't matter which branch I'm running on. |
Summary:
This PR adds support for
display: contents
property. The approach it takes to implement is to make allYogaLayoutableShadowNodes
with the property set to be skipped when building the Yoga tree, like so (green node is one withdisplay: contents
):To accomplish this, the way
YogaLayoutableShadowNode
stores its layoutable children. Before this PR it had a list of all direct children that were alsoYogaLayoutableShadowNode
but this PR breaks the assumption that there is a 1:1 relation between Yoga tree andYogaLayoutableShadowNodes
. To take this into account, I've changed the list of direct children to a list of pairs - one of the nodes in the pair is the direct child of the node and the other is a node that's a direct child of the ownersYogaNode
. The two pointers differ only if the childYogaLayoutableShadowNode
hasdisplay: contents
set, otherwise they point to the same node. In the above example the list for the nodeA
would look like this:[(B, C), (B, D), (E, E)]
, where the first node represents a direct child of the shadow node, and the second node represents the owner of the direct child of yoga node. Due to this change, the logic of all methods referencing the list had to be updated to reflect this change.ViewShadowNode
andConcreteViewShadowNode
were updated to always be flattened when they havedisplay: contents
set.TextInputShadowNode
was updated to setHidden
trait when havingdisplay: contents
set as according to the spec it should be treated asdisplay: none
.In most places in
YogaLayoutableShadowNodes
the logic itself didn't change - instead of operating directly on the noderunForEveryConcreteSubtree
is used and instead of appending the node directly toyogaLayoutableChildren_
a pair is added.Unless the node has
display: contents
setrunForEveryConcreteSubtree
will be executed only for that node, otherwise it will recursively traverse the subtree and run the provided lambda for every non-contents subtree found:If the method were to be called with the node
A
in the above tree, the lambda would be executed for nodesC
,D
, andE
.Significant changes were made to the
replaceChild
method. Previously it was always called with a direct child of the node, so the base method could have been used. Now it may be called with a descendant that's not a direct child. To accomodate that, the logic is split in two branches:cloneTree
method is used on an ancestor that's a direct child of the node which child is being replaced.cloneTree
is responsible to replace the node in the subtree, which then is being inserted into the node using the basereplaceChild
method. In the above graph, ifreplaceChild
were called on nodeA
with nodeC'
nodeB
would be cloned in a way that would replaceC
withC'
resulting in subtreeB'
being created. ThenB
would be replaced withB'
using basereplaceChild
method.After the tree structure is updated, it may be necessary to also update
yogaLayoutableChildren_
as the subtree structure may have been changed. Previously it had two branches:YogaLayoutableShadowNode
, erase itYogaLayoutableShadowNode
, replace it in place in the listAfter the change, replacing a child may result in multiple nodes in
yogaLayoutableChildren_
being changed. The split into two branches remains:YogaLayoutableShadowNode
, erase every pair that contained the old node from the listdisplay: contents
) and replace all the nodes in place. If there are more nodes than before, they will be inserted in order so that entries sharing the direct child pointer are clustered together. If there are less nodes than before, the excess nodes will be erased from the list.Similarly, there were some changes necessary in
cloneChildInPlace
method - previously it was cloning a node at specified index and replacing it. Now, sinceyogaLayoutableChildren_
don't have to be direct children of the node it needs to usecloneTree
method to clone entire subtree that contains the relevant node.Notes:
Display::Contents
from Yoga since the two places wheredisplay
property is stored are the yoga node and props which hold yoga styles.Changelog:
[GENERAL] [ADDED] - Added support for
display: contents
Test Plan:
So far I've been testing on relatively simple snippets like this one and on entirety of RNTester by inserting views with `display: contents` in random places and seeing if anything breaks.