From 35db60440fc95b3d55159bf002520fdedaefff15 Mon Sep 17 00:00:00 2001 From: MJ Studio Date: Fri, 9 Oct 2026 21:17:23 +0900 Subject: [PATCH 1/4] fix(ios): inherit text fonts for inline attachments --- .../renderer/textlayoutmanager/RCTAttributedTextUtils.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 95ff5a0d180e..24c945321db2 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -415,7 +415,13 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) attachment.image = placeholderImage; attachment.bounds = bounds; - return [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy]; + NSMutableAttributedString *attributedString = + [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy]; + UIFont *font = RCTEffectiveFontFromTextAttributes(fragment.textAttributes); + if (font) { + [attributedString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, attributedString.length)]; + } + return attributedString; } else { NSString *decoded = [[NSString alloc] initWithBytes:fragment.string.data() length:fragment.string.size() From 47c1e8c0b0ab6f994b55b3510ed614f114fd753c Mon Sep 17 00:00:00 2001 From: MJ Studio Date: Sat, 10 Oct 2026 02:48:32 +0900 Subject: [PATCH 2/4] fix(ios): apply baseline offsets to inline view layout --- .../renderer/textlayoutmanager/RCTTextLayoutManager.mm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 33306db54922..62b5f4b720f5 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -643,13 +643,15 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; CGRect frame; - UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName - atIndex:0 - effectiveRange:nil]; + NSDictionary *attributes = [textStorage attributesAtIndex:range.location + effectiveRange:nil]; + UIFont *font = attributes[NSFontAttributeName]; + CGFloat baselineOffset = [attributes[NSBaselineOffsetAttributeName] doubleValue]; frame = { .origin = {glyphRect.origin.x, - glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender}, + glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender - + baselineOffset}, .size = attachmentSize}; auto rect = facebook::react::Rect{ From 95d557afb09b76659ff89a3b49b4aba511cc13e2 Mon Sep 17 00:00:00 2001 From: MJ Studio Date: Sat, 14 Nov 2026 00:18:59 +0900 Subject: [PATCH 3/4] test(ios): add inline view baseline reproduction to RNTester --- .../js/examples/Text/TextExample.ios.js | 6 + .../Text/TextInlineViewBaselineExample.js | 198 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index ff5ff29a097f..e89d1e95a928 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -15,6 +15,7 @@ import type {RNTesterModule} from '../../types/RNTesterTypes'; import hotdog from '../../assets/hotdog.jpg'; import RNTesterText from '../../components/RNTesterText'; import TextLegend from '../../components/TextLegend'; +import {TextInlineViewBaselineExample} from './TextInlineViewBaselineExample'; import TextSharedExamples from './TextSharedExamples'; import TextWidthModeExample from './TextWidthModeExample'; @@ -1310,6 +1311,11 @@ const examples = [ title: 'Inline views', render: (): React.Node => , }, + { + title: 'Inline view baseline alignment', + name: 'inline-view-baseline', + render: (): React.Node => , + }, { title: 'Inline image/view clipped by ', render: (): React.Node => , diff --git a/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js b/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js new file mode 100644 index 000000000000..25b000895098 --- /dev/null +++ b/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js @@ -0,0 +1,198 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {TextStyle} from 'react-native'; + +import RNTesterText from '../../components/RNTesterText'; +import * as React from 'react'; +import { + ScrollView, + StyleSheet, + Text, + View, + useWindowDimensions, +} from 'react-native'; + +const INLINE_SIZE = 18; +const DEFAULT_FONT_SIZE = 20; +const MEDIUM_FONT_SIZE = 24; +const LARGE_FONT_SIZE = 32; +const TALL_LINE_HEIGHT = 48; +const FONT_FAMILY = 'Helvetica'; +const SERIF_FONT_FAMILY = 'TimesNewRomanPSMT'; +const SPACING = 8; + +type Sample = Readonly<{ + name: string, + textStyle: TextStyle, + height?: number, + isNested?: boolean, + isWrapped?: boolean, + isScaled?: boolean, +}>; + +const SAMPLES: Sample[] = [ + { + name: 'Helvetica 12 (control)', + textStyle: {fontFamily: FONT_FAMILY, fontSize: 12}, + }, + {name: 'System 20', textStyle: {fontSize: DEFAULT_FONT_SIZE}}, + { + name: 'Helvetica 32', + textStyle: {fontFamily: FONT_FAMILY, fontSize: LARGE_FONT_SIZE}, + }, + { + name: 'Times New Roman 32', + textStyle: {fontFamily: SERIF_FONT_FAMILY, fontSize: LARGE_FONT_SIZE}, + }, + { + name: 'Courier bold 24', + textStyle: { + fontFamily: 'Courier', + fontSize: MEDIUM_FONT_SIZE, + fontWeight: 'bold', + }, + }, + { + name: 'Helvetica 32 / lineHeight 48', + textStyle: { + fontFamily: FONT_FAMILY, + fontSize: LARGE_FONT_SIZE, + lineHeight: TALL_LINE_HEIGHT, + }, + }, + { + name: 'Nested Times New Roman 32', + textStyle: {fontFamily: SERIF_FONT_FAMILY, fontSize: LARGE_FONT_SIZE}, + isNested: true, + }, + { + name: 'Tall View / Helvetica 20', + textStyle: {fontFamily: FONT_FAMILY, fontSize: DEFAULT_FONT_SIZE}, + height: TALL_LINE_HEIGHT, + }, + { + name: 'Wrapped / Helvetica 24', + textStyle: {fontFamily: FONT_FAMILY, fontSize: MEDIUM_FONT_SIZE}, + isWrapped: true, + }, + { + name: 'Scaled / Helvetica 20', + textStyle: {fontFamily: FONT_FAMILY, fontSize: DEFAULT_FONT_SIZE}, + isScaled: true, + }, +]; + +function InlineViewBaselineSample({ + name, + textStyle, + height = INLINE_SIZE, + isNested = false, + isWrapped = false, + isScaled = false, +}: Sample): React.Node { + const [baseline, setBaseline] = React.useState(); + const [bottom, setBottom] = React.useState(); + const inlineRef = React.useRef | null>(null); + + React.useEffect(() => { + inlineRef.current?.measure((x, y, width, measuredHeight) => { + setBottom(measuredHeight > 0 ? y + measuredHeight : undefined); + }); + }, [baseline]); + + const error = + baseline != null && bottom != null ? bottom - baseline : undefined; + const content = ( + <> + {'Hg '} + + {' Hg'} + + ); + + return ( + + + {name} + + + { + const line = event.nativeEvent.lines[isWrapped ? 1 : 0]; + if (line != null) { + setBaseline(line.y + line.ascender); + } + }}> + {isWrapped ? 'First line\n' : ''} + {isNested ? {content} : content} + + {baseline != null ? ( + + ) : null} + + + {error == null + ? baseline == null + ? 'Measuring...' + : 'View measurement unavailable' + : `baseline ${baseline?.toFixed(3)} | bottom ${bottom?.toFixed(3)} | error ${error.toFixed(3)} pt`} + + + ); +} + +export function TextInlineViewBaselineExample(): React.Node { + const {scale, fontScale} = useWindowDimensions(); + + return ( + + + Each blue View should end at the red text baseline. Allow one physical + pixel for rounding. Enlarge the system text size to check the scaled + sample. + + + {`One pixel: ${(1 / scale).toFixed(3)} pt | font scale: ${fontScale.toFixed(3)} (last sample only)`} + + {SAMPLES.map(sample => ( + + ))} + + ); +} + +const styles = StyleSheet.create({ + example: {gap: SPACING, padding: SPACING * 2}, + sample: {gap: SPACING / 2}, + paragraph: {backgroundColor: '#f1f5f9'}, + text: {color: 'black'}, + inline: {width: INLINE_SIZE, backgroundColor: '#2563eb'}, + baseline: { + position: 'absolute', + left: 0, + right: 0, + height: 1, + backgroundColor: '#dc2626', + }, + nestedParent: {fontFamily: FONT_FAMILY, fontSize: 16}, + metrics: {fontSize: 10, fontFamily: 'Menlo'}, +}); From d969690bee171f680d9f66ad4eae9cf2a9d1df65 Mon Sep 17 00:00:00 2001 From: MJ Studio Date: Sat, 14 Nov 2026 00:56:16 +0900 Subject: [PATCH 4/4] fix(rntester): narrow measurements before formatting --- .../Text/TextInlineViewBaselineExample.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js b/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js index 25b000895098..6047d00c3d65 100644 --- a/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js +++ b/packages/rn-tester/js/examples/Text/TextInlineViewBaselineExample.js @@ -28,6 +28,7 @@ const TALL_LINE_HEIGHT = 48; const FONT_FAMILY = 'Helvetica'; const SERIF_FONT_FAMILY = 'TimesNewRomanPSMT'; const SPACING = 8; +const DISPLAY_DECIMAL_PLACES = 3; type Sample = Readonly<{ name: string, @@ -108,8 +109,6 @@ function InlineViewBaselineSample({ }); }, [baseline]); - const error = - baseline != null && bottom != null ? bottom - baseline : undefined; const content = ( <> {'Hg '} @@ -147,11 +146,11 @@ function InlineViewBaselineSample({ allowFontScaling={false} testID={`${name}-metrics`} style={styles.metrics}> - {error == null - ? baseline == null - ? 'Measuring...' - : 'View measurement unavailable' - : `baseline ${baseline?.toFixed(3)} | bottom ${bottom?.toFixed(3)} | error ${error.toFixed(3)} pt`} + {baseline == null + ? 'Measuring...' + : bottom == null + ? 'View measurement unavailable' + : `baseline ${baseline.toFixed(DISPLAY_DECIMAL_PLACES)} | bottom ${bottom.toFixed(DISPLAY_DECIMAL_PLACES)} | error ${(bottom - baseline).toFixed(DISPLAY_DECIMAL_PLACES)} pt`} ); @@ -171,7 +170,7 @@ export function TextInlineViewBaselineExample(): React.Node { sample. - {`One pixel: ${(1 / scale).toFixed(3)} pt | font scale: ${fontScale.toFixed(3)} (last sample only)`} + {`One pixel: ${(1 / scale).toFixed(DISPLAY_DECIMAL_PLACES)} pt | font scale: ${fontScale.toFixed(DISPLAY_DECIMAL_PLACES)} (last sample only)`} {SAMPLES.map(sample => (