-
Notifications
You must be signed in to change notification settings - Fork 24.4k
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
Implement baseline alignment function on the new architecture #45102
Implement baseline alignment function on the new architecture #45102
Conversation
/rebase |
Autorebase failed as we're doing GHA work at the moment. @j-piasecki if you could rebase the CI should be green |
763988c
to
00ccf02
Compare
Base commit: 8c8c77b |
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 add an RNTester example for this? edit: missed we had the existing one shown in test plan
Layout layout = | ||
createLayout(context, attributedString, paragraphAttributes, width, height, null); |
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 will recreate AttributedString MappBuffer, Spannable, and relayout, which can be expensive. It would be great if we could reuse some of these, but I'm not sure this is nice to do with current interface, and needing to calculate baseline is not the most common case.
At some point in the future, I am looking to change this code a bit, so that we use PrecomputedText
, which associates some glyph-level measurement information with the Spannable when we layout (so that later, the UI thread TextView does not need to relayout after Fabric layout, which can cause scroll jank). I haven't thought about how to organize this yet though.
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 there anything I should do regarding this?
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.
For now, we can leave it. I have some ideas here (like riffing off the spannable IDs used in other places for reuse, or batching baseline and measure together), that I might try to take a stab at later.
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.
Hmm, though I think the Fabric LineMeasurements
would contain everything already needed to determine a baseline.
Right now I don't remember that we store line measurements against contraints (my guess it we don't), but it means baseline calculation could rely on information already retrieved most of the time, by measure.
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.
Hmm, though I think the Fabric LineMeasurements would contain everything already needed to determine a baseline.
I'm pretty sure you're right
Right now I don't remember that we store line measurements against contraints (my guess it we don't), but it means baseline calculation could rely on information already retrieved most of the time, by measure.
It's not cached at the moment. Is this close to what you have in mind: 0ea302c?
@@ -504,6 +504,20 @@ private NativeArray measureLines( | |||
PixelUtil.toPixelFromDIP(height)); | |||
} | |||
|
|||
@SuppressWarnings("unused") | |||
private float getLastBaseline( |
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.
Does Paper use last line? Spec kinda seems to suggest inline-flex
(how RN paragraph mostly works) would align to first line of multiline context. https://www.w3.org/TR/css-inline-3/#baseline-source
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.
Yes it does:
Line 177 in 4a8f0ee
return layout.getLineBaseline(layout.getLineCount() - 1); |
float maxDescent = 0; | ||
for (int i = 0; i < layout.getLineCount(); i++) { | ||
if (maxDescent < layout.getLineDescent(i)) { | ||
maxDescent = layout.getLineDescent(i); | ||
} | ||
} |
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 loop through these? My assumption would be that we always want to use descent of the last line (or, first line, depending on above comment).
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 to align with the implementation used on iOS:
react-native/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm
Lines 359 to 368 in 4a8f0ee
[attributedText enumerateAttribute:NSFontAttributeName | |
inRange:NSMakeRange(0, attributedText.length) | |
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired | |
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { | |
if (maximumDescender > font.descender) { | |
maximumDescender = font.descender; | |
} | |
}]; | |
return size.height + maximumDescender; |
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 naming is confusing me here a little bit. I am wondering if these are doing the same things.
A "descender" is part of text that goes under the baseline. So, getting lowest descender, is I think trying to get the bottom of the text. Though, this isn't really the baseline.
But a "descent" usually is the distance between the baseline and the bottom of the line-box. StackOverflow suggests that, this value on Android layout does not have any sort of per-line offset. https://stackoverflow.com/a/43691403
I think the logic works for single-line, but I don't think the Android logic will work correctly for multiline containers.
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.
My bet is that iOS might have been using largest font, to calculate offset from bottom, to approximate the last line height, that we have the direct access to on Android.
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.
There is a difference though, when padding is applied to TextInput
. It's included in height
but the Java part is unaware of it.
virtual Float firstBaseline(Size size) const; | ||
virtual Float lastBaseline(Size size) const; | ||
virtual Float firstBaseline(const LayoutContext& layoutContext, Size size) const; | ||
virtual Float lastBaseline(const LayoutContext& layoutContext, Size size) const; |
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 existing code, but I think this could potentially just be a single function, since rules around multiple baselines, and which to pick, is text-specific concept/implementation detail. If we only give a single baseline function to Yoga, having one baseline function to override which isn't called could be confusing.
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.
Do you want me to remove the firstBaseline
?
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 had in mind replacing both with a single baseline
function, where line selection would be a detail of it.
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 - f576413
The way Paper is handling multi-line baseline selection in flex containers seems inconsistent with browsers/flexbox spec. In browser, first-line is chosen. https://jsfiddle.net/jq49r8hb/ I suspect we could get away with changing the behavior in RN to align with the spec and browsers. But I'm curious what @cortinico thinks about this. |
@j-piasecki I think this is looking good. Have we tested/do we have validation for multi-line? I still kinda think we should switch Paper behavior for web behavior though, now that we have the chance to 😅. |
@cortinico has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
@j-piasecki this is failing to build internally with:
|
That's weird, it looks like it's pointing here: Lines 188 to 192 in 08c8d8d
getLastBaseline at all in the codebase at the moment. Is it possible that it's caused by a stale cache?
|
@j-piasecki my bad I copied the error message form a old revision. The error is still: [...]\packages\react-native\ReactCommon\react\renderer\components\text\ParagraphShadowNode.cpp(188,30): error: no member named 'baseline' in 'facebook::react::TextLayoutManager'
return textLayoutManager_->baseline(
~~~~~~~~~~~~~~~~~~~~^ |
@@ -303,12 +305,12 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage | |||
|
|||
CGSize attachmentSize = attachment.bounds.size; | |||
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; | |||
|
|||
CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; | |||
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; |
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 variable is unused now and is causing various iOS builds to fail (we do have -Wall
everywhere)
I don't see why it would fail there. All One thing I noticed is that I was using |
@cortinico has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Ok so the issue is that we do have other |
@j-piasecki Sorry, I missed reading this in the last iteration. In an inline/text context, items are by default aligned so that their baselines mare matched. I think the "cascading" problem is an artifact of always treating the baseline of the inline view as its bottom. A flexbox container's baseline is set based on its children. So, the baseline of that blue "View", should really match the baseline of the text inside of it.
Yoga does implement this algorithm, for determining the baseline of a child when using Not something for this PR, but would definitely be welcome as followup if you ever feel like doing more around this area. |
...erer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
Outdated
Show resolved
Hide resolved
...erer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
Show resolved
Hide resolved
@cortinico has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
That's exactly what's happening.
Sure, I would be up for it, though I don't think it would be contained to the React Native's part of text layout. Every inline view is passed as an attachment which is essentially a rectangle without any information about it's children or baseline. Moreover, it looks like inline views are aligned on the last baseline instead of the first one: https://jsfiddle.net/tgkdr63s/ |
If you change |
From https://jsfiddle.net/jq49r8hb/ I think the first baseline behavior will also still happen when aligning |
@cortinico merged this pull request in 2932c0f. |
This pull request was successfully merged by @j-piasecki in 2932c0f. When will my fix make it into a release? | How to file a pick request? |
why is something like this not possible still? https://jsfiddle.net/yhc43e9w/1/ Aligning the box in the center. |
Summary: In #45102 I've implemented a baseline alignment function for the new architecture. I've noticed one thing I've missed previously - `locationForGlyphAtIndex` is [relative to the line fragment](https://developer.apple.com/documentation/appkit/nslayoutmanager/1403239-locationforglyphatindex), not the container. This means that the attachments would be put in the wrong place in multiline text. This PR fixes that by adding the position of the entire line to the attachment position. ## Changelog: [IOS] [FIXED] - Fixed baseline attachment position in multiline text Pull Request resolved: #46172 Test Plan: Checked on relevant example of RNTester. |Before|After| |-|-| |<img width="546" alt="Screenshot 2024-08-22 at 15 53 14" src="https://github.com/user-attachments/assets/c1861655-9253-44fd-9f2f-796aff83df1e">|<img width="546" alt="Screenshot 2024-08-22 at 15 52 37" src="https://github.com/user-attachments/assets/27f07bc7-a8f6-4696-a414-815e7ece642c">| Reviewed By: andrewdacenko Differential Revision: D61662006 Pulled By: cipolleschi fbshipit-source-id: 5eafdae1800c06d9fc61bfac99584e6e25a05c24
Summary:
On the new architecture, the setup that would allow Yoga to read the baseline of a node was missing. This PR adds it:
BaselineYogaNode
that marks a node as having a custom baseline functionyogaNodeBaselineCallbackConnector
that's responsible for allowing Yoga to call baseline function on the nodelastBaseline
andfirstBaseline
to acceptLayoutContext
as the first argument, which is necessary to build an attributed stringlastBaseline
that's invoked byyogaNodeBaselineCallbackConnector
TextLayoutManagers
, using the same approach on both Android and iOS (this differs from the old architecture where calculations were different)Changelog:
[GENERAL] [FIXED] - Fixed
alignItems: 'baseline'
not working correctly on the new architectureTest Plan:
Tested on the relevant part of RNTester:
Android
iOS