Repository navigation
Conversation
Text under a Modal, an InputAccessoryView or a react-native-screens screen kept its old size after a live font scale change on iOS. setSize(), which those descriptors call from adopt(), dirties a node without its ancestors, and markDirtyAndPropagate() stops at the first dirty owner. The clean parent above it then reuses its cached layout, so the subtree never gets the new metrics. Walk every owner in the font scale block of configureYogaTree instead, and stop at a display: none owner, which lays out no children.
|
Hi @simongt! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
javache
left a comment
There was a problem hiding this comment.
configureYogaTree is already supposed to walk the tree, so adding an extra walk here seems incorrect.
How about fixing ModalHostView so it recurses into its children for these calls, even if it's already dirty? Or changing the top-level bailout for IsDirty here? The current one does not seem sufficient to handle this.
configureYogaTree already walks the tree, so the extra walk over the owners is gone. Instead, apply the rule of updateYogaChildren() as the walk unwinds: after configuring a child, a node marks itself dirty when the child came back dirty, and display: none nodes are skipped. This covers clones dirtied by setSize() in a descriptor's adopt(), such as a Modal, and clones dirtied by completeClone(), such as a TextInput with no Modal above it. Replace the hidden-node test, which passed without the fix, and drop the unchanged-scale guard, which could not fail. Add a test for an input-like node without a Modal, and one that a hidden subtree is not laid out until it is shown.
|
Hey @javache, thanks for looking this over.
Moved the fix into the existing One clarification on the root cause: the walk down is fine, it reaches the text under the modal and marks it dirty. The break is on the way up. When the font scale changes, the modal gets cloned in place, and its descriptor's
On your suggestions:
So now, as the existing walk unwinds, a node marks itself dirty if a child it just configured came back dirty, the same rule Ok to fold the |
Upstream removed enableFontScaleChangesUpdatingLayout (react#59389). The conflict is in the font block of configureYogaTree: keep the upstream condition without the flag, and keep our comment on it. The post-order dirty check in the child loop also used the flag, so it is ungated now, like the font block.
|
Merged main to pick up the font scale flag removal. The new check in the child loop was behind the same flag, so it's ungated too, same as the font block. The modal and input tests still fail without it and pass with it. Since this file has been changing a lot lately, I'm a bit worried the PR will keep drifting and picking up conflicts the longer it sits. Could you please take another look when you get a chance? Thanks @javache |
| // An in-place clone can come back dirty without dirtying this node, from | ||
| // `setSize()` in a descriptor's `adopt()` or from `completeClone()`. | ||
| // Apply the rule of `updateYogaChildren()` so Yoga doesn't skip it. | ||
| if (yogaLayoutableChildren_[i]->yogaNode_.isDirty() && |
There was a problem hiding this comment.
Make it explicit that we need to re-read yogaLayoutableChildren_[i] because cloneChildInPlace may have changed.
| if (yogaLayoutableChildren_[i]->yogaNode_.isDirty() && | |
| const auto& updatedYogaNode = yogaLayoutableChildren_[i]->yogaNode_; | |
| if (updatedYogaNode.isDirty() && |
There was a problem hiding this comment.
Applied, thanks (child still points at the node from before the clone, so the name makes the re-read deliberate).
javache
left a comment
There was a problem hiding this comment.
One non-blocking test suggestion: cover an RTL root with a direction override under swapLeftAndRightInRTL, followed by an unrelated commit with unchanged font scale. This PR explicitly changes ancestor layout behaviour in that case, but the new tests don't cover it. Assert stable geometry and the expected affectedNodes behaviour.
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D128040309. |
`cloneChildInPlace()` replaces `yogaLayoutableChildren_[i]`, so `child` still refers to the node from before the clone. Name the re-read node so the post-order dirty check visibly reads the configured clone.
Under an RTL root that swaps left and right, a `direction: ltr` subtree stores no swap in its layout metrics, so `configureYogaTree` configures it again on every pass and clones its children in place. An input-like leaf comes back dirty from that clone, and the post-order dirty check lays it out again. Pin that the geometry stays the same and which nodes are affected.
`configureYogaTree` skips a configured child when its stored `wasLeftAndRightSwapped` matches the swap passed down from the root. `layout()` stored the child's own direction instead, so a `direction` override under a root that swaps left and right never matched. That subtree was configured again and cloned in place on every pass, and a clone that came back dirty was laid out again. `wasLeftAndRightSwapped` has no other reader. Store the swap that was applied, taken from the parent, so the subtree is configured again only when the swap changes. The RTL test now flips the root to RTL, then asserts that an unrelated commit keeps the geometry, leaves the leaf out of `affectedNodes`, and does not clone it.
Added the test (which showed that subtree re-configured every pass because |
Summary:
Fixes #59375.
On iOS, text inside a
<Modal>keeps its old size after a live text size change, and the same happens under react-native-screens screens. On the font scale pass,configureYogaTreeclones the modal in place, and its descriptor'ssetSize()marks the clone dirty without its parent. The text's dirty flag then stops at the modal on its way up, the modal's parent looks unchanged, and Yoga reuses its cached layout for the whole subtree. An input-like node with no modal above it goes stale the same way, sincecompleteClone()dirties its in-place clone without the parent.configureYogaTreenow applies the ruleupdateYogaChildren()uses when a parent is rebuilt: as the walk unwinds, a node marks itself dirty if a child it just configured came back dirty. Hidden (display: none) nodes are skipped, since they lay out no children.The RTL test asked for in review also showed why a
directionoverride under a root that swaps left and right (swapLeftAndRightInRTL) was configured again on every pass.configureYogaTreeskips a configured child when its storedwasLeftAndRightSwappedmatches the swap passed down from the root, butlayout()stored the child's own direction, so an override never matched. Each pass cloned that subtree's children in place, and with the rule above, a clone that came back dirty got laid out again.layout()now stores the swap that was applied, taken from the parent, so the subtree is only configured again when that swap changes. Nothing else readswasLeftAndRightSwapped.Changelog:
[GENERAL] [FIXED] - Text inside Modal now updates after a live font scale change
Test Plan:
Added four tests to
ViewTest.cpp, on aFontScaleLayoutTestfixture with a text-like and an input-like measurable leaf whose size follows the font scale:fontScaleChangeLaysOutMeasurableNodesInsideModal: both leaves inside aModalHostView, at font scale 1, 1.5, then 1. Fails without the change (the stored scale stays 1 and the height stays 10 instead of 15), passes with it.fontScaleChangeLaysOutInputLikeNodeWithoutModal: the input-like leaf with no modal. Fails without the change, passes with it.fontScaleChangeSkipsHiddenSubtreeUntilShown: a font scale change under adisplay: noneview doesn't lay the hidden view out, and the leaf gets the new scale once it's shown. Passes without the change, and fails if thedisplay: nonecheck is removed.rtlDirectionOverrideIsConfiguredOnlyWhenTheSwapChanges: adirection: ltrsubtree with the input-like leaf under a root that swaps left and right. Turning the root RTL configures the subtree again and lays the leaf out. An unrelated commit after that keeps the geometry, doesn't clone the leaf, and leaves it out ofaffectedNodes. Fails without thelayout()change, passes with it.With only
setSize()andsetPositionType()dirtying on change instead, the input leaf in the modal test still fails.There's no OSS runner for these gtests, so I ran
ViewTest.cppwith a local CMake and googletest setup on macOS (Homebrew folly and glog rather than the pinned versions), with-Wall -Wextra -Werroron the two changed files: 20/20 pass with the change. Formatted with clang-format 21.0.0 (the repo pins 21.1.2).On an iPhone 17 Pro simulator (iOS 26.5), with React Native built from source:
direction: 'ltr'view next to a view whose width changes over six commits. On stock 0.88.0 each of those commits cloned the two children of thedirection: 'ltr'view in place; with this change none did. A fresh launch matches stock 0.88.0 at the default size and at XXXL, and a live change to XXXL and back matches a fresh launch.Not tested on Android.