Skip to content
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 Dynamic Type support for iOS (Paper and Fabric) #35017

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions Libraries/Text/BaseText/RCTBaseTextViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ - (RCTShadowView *)shadowView
RCT_REMAP_SHADOW_PROPERTY(fontStyle, textAttributes.fontStyle, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontVariant, textAttributes.fontVariant, NSArray)
RCT_REMAP_SHADOW_PROPERTY(allowFontScaling, textAttributes.allowFontScaling, BOOL)
RCT_REMAP_SHADOW_PROPERTY(dynamicTypeRamp, textAttributes.dynamicTypeRamp, RCTDynamicTypeRamp)
RCT_REMAP_SHADOW_PROPERTY(maxFontSizeMultiplier, textAttributes.maxFontSizeMultiplier, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, textAttributes.letterSpacing, CGFloat)
// Paragraph Styles
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Text/RCTTextAttributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import <UIKit/UIKit.h>

#import <React/RCTDynamicTypeRamp.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this error on an internal build script:

packages/rn-tester/Pods/Headers/Public/React-Core/React/RCTTextAttributes.h:10:9: fatal error: 'React/RCTDynamicTypeRamp.h' file not found
#import <React/RCTDynamicTypeRamp.h>
^~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

RCTTextDecorationLineType lives in React/Views. Wondering if this header should go there as well, vs if there is some internal bits we need to update.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what RCTTextDecorationLineType has to do with any of this. Considering that RCTDynamicTypeRamp.h is a newly created file with this PR, I'm guessing 0254c3a should help here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NickGerleman Can you confirm that this fix works?

#import <React/RCTTextDecorationLineType.h>

#import "RCTTextTransform.h"
Expand Down Expand Up @@ -36,6 +37,7 @@ extern NSString *const RCTTextAttributesTagAttributeName;
@property (nonatomic, copy, nullable) NSString *fontStyle;
@property (nonatomic, copy, nullable) NSArray<NSString *> *fontVariant;
@property (nonatomic, assign) BOOL allowFontScaling;
@property (nonatomic, assign) RCTDynamicTypeRamp dynamicTypeRamp;
@property (nonatomic, assign) CGFloat letterSpacing;
// Paragraph Styles
@property (nonatomic, assign) CGFloat lineHeight;
Expand Down
9 changes: 8 additions & 1 deletion Libraries/Text/RCTTextAttributes.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes
_fontStyle = textAttributes->_fontStyle ?: _fontStyle;
_fontVariant = textAttributes->_fontVariant ?: _fontVariant;
_allowFontScaling = textAttributes->_allowFontScaling || _allowFontScaling; // *
_dynamicTypeRamp = textAttributes->_dynamicTypeRamp != RCTDynamicTypeRampUndefined ? textAttributes->_dynamicTypeRamp : _dynamicTypeRamp;
_letterSpacing = !isnan(textAttributes->_letterSpacing) ? textAttributes->_letterSpacing : _letterSpacing;

// Paragraph Styles
Expand Down Expand Up @@ -230,6 +231,12 @@ - (CGFloat)effectiveFontSizeMultiplier

if (fontScalingEnabled) {
CGFloat fontSizeMultiplier = !isnan(_fontSizeMultiplier) ? _fontSizeMultiplier : 1.0;
if (_dynamicTypeRamp != RCTDynamicTypeRampUndefined) {
UIFontMetrics *fontMetrics = RCTUIFontMetricsForDynamicTypeRamp(_dynamicTypeRamp);
// Using a specific font size reduces rounding errors from -scaledValueForValue:
CGFloat requestedSize = isnan(_fontSize) ? RCTBaseSizeForDynamicTypeRamp(_dynamicTypeRamp) : _fontSize;
fontSizeMultiplier = [fontMetrics scaledValueForValue:requestedSize] / requestedSize;
}
CGFloat maxFontSizeMultiplier = !isnan(_maxFontSizeMultiplier) ? _maxFontSizeMultiplier : 0.0;
return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier;
} else {
Expand Down Expand Up @@ -324,7 +331,7 @@ - (BOOL)isEqual:(RCTTextAttributes *)textAttributes
RCTTextAttributesCompareFloats(_fontSizeMultiplier) && RCTTextAttributesCompareFloats(_maxFontSizeMultiplier) &&
RCTTextAttributesCompareStrings(_fontWeight) && RCTTextAttributesCompareObjects(_fontStyle) &&
RCTTextAttributesCompareObjects(_fontVariant) && RCTTextAttributesCompareOthers(_allowFontScaling) &&
RCTTextAttributesCompareFloats(_letterSpacing) &&
RCTTextAttributesCompareOthers(_dynamicTypeRamp) && RCTTextAttributesCompareFloats(_letterSpacing) &&
// Paragraph Styles
RCTTextAttributesCompareFloats(_lineHeight) && RCTTextAttributesCompareFloats(_alignment) &&
RCTTextAttributesCompareOthers(_baseWritingDirection) && RCTTextAttributesCompareOthers(_lineBreakStrategy) &&
Expand Down
17 changes: 17 additions & 0 deletions Libraries/Text/Text.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ export interface TextPropsIOS {
*/
adjustsFontSizeToFit?: boolean | undefined;

/**
* The Dynamic Text scale ramp to apply to this element on iOS.
*/
dynamicTypeRamp?:
| 'caption2'
| 'caption1'
| 'footnote'
| 'subheadline'
| 'callout'
| 'body'
| 'headline'
| 'title3'
| 'title2'
| 'title1'
| 'largeTitle'
| undefined;

/**
* Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
*/
Expand Down
36 changes: 36 additions & 0 deletions Libraries/Text/Text/RCTDynamicTypeRamp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.
*/

#import <Foundation/Foundation.h>

#import <React/RCTConvert.h>

typedef NS_ENUM(NSInteger, RCTDynamicTypeRamp) {
RCTDynamicTypeRampUndefined,
RCTDynamicTypeRampCaption2,
RCTDynamicTypeRampCaption1,
RCTDynamicTypeRampFootnote,
RCTDynamicTypeRampSubheadline,
RCTDynamicTypeRampCallout,
RCTDynamicTypeRampBody,
RCTDynamicTypeRampHeadline,
RCTDynamicTypeRampTitle3,
RCTDynamicTypeRampTitle2,
RCTDynamicTypeRampTitle1,
RCTDynamicTypeRampLargeTitle
};

@interface RCTConvert (DynamicTypeRamp)

+ (RCTDynamicTypeRamp)RCTDynamicTypeRamp:(nullable id)json;

@end

/// Generates a `UIFontMetrics` instance representing a particular Dynamic Type ramp.
UIFontMetrics * _Nonnull RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp);
/// The "reference" size for a particular font scale ramp, equal to a text element's size under default text size settings.
CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp);
73 changes: 73 additions & 0 deletions Libraries/Text/Text/RCTDynamicTypeRamp.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.
*/

#import <React/RCTDynamicTypeRamp.h>

@implementation RCTConvert (DynamicTypeRamp)

RCT_ENUM_CONVERTER(RCTDynamicTypeRamp, (@{
@"caption2": @(RCTDynamicTypeRampCaption2),
@"caption1": @(RCTDynamicTypeRampCaption1),
@"footnote": @(RCTDynamicTypeRampFootnote),
@"subheadline": @(RCTDynamicTypeRampSubheadline),
@"callout": @(RCTDynamicTypeRampCallout),
@"body": @(RCTDynamicTypeRampBody),
@"headline": @(RCTDynamicTypeRampHeadline),
@"title3": @(RCTDynamicTypeRampTitle3),
@"title2": @(RCTDynamicTypeRampTitle2),
@"title1": @(RCTDynamicTypeRampTitle1),
@"largeTitle": @(RCTDynamicTypeRampLargeTitle),
}), RCTDynamicTypeRampUndefined, integerValue)

@end

UIFontMetrics *RCTUIFontMetricsForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp) {
static NSDictionary<NSNumber *, UIFontTextStyle> *mapping;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mapping = @{
@(RCTDynamicTypeRampCaption2): UIFontTextStyleCaption2,
@(RCTDynamicTypeRampCaption1): UIFontTextStyleCaption1,
@(RCTDynamicTypeRampFootnote): UIFontTextStyleFootnote,
@(RCTDynamicTypeRampSubheadline): UIFontTextStyleSubheadline,
@(RCTDynamicTypeRampCallout): UIFontTextStyleCallout,
@(RCTDynamicTypeRampBody): UIFontTextStyleBody,
@(RCTDynamicTypeRampHeadline): UIFontTextStyleHeadline,
@(RCTDynamicTypeRampTitle3): UIFontTextStyleTitle3,
@(RCTDynamicTypeRampTitle2): UIFontTextStyleTitle2,
@(RCTDynamicTypeRampTitle1): UIFontTextStyleTitle1,
@(RCTDynamicTypeRampLargeTitle): UIFontTextStyleLargeTitle,
};
});

id textStyle = mapping[@(dynamicTypeRamp)] ?: UIFontTextStyleBody; // Default to body if we don't recognize the specified ramp
return [UIFontMetrics metricsForTextStyle:textStyle];
}

CGFloat RCTBaseSizeForDynamicTypeRamp(RCTDynamicTypeRamp dynamicTypeRamp) {
static NSDictionary<NSNumber *, NSNumber *> *mapping;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Values taken from https://developer.apple.com/design/human-interface-guidelines/foundations/typography/
mapping = @{
@(RCTDynamicTypeRampCaption2): @11,
@(RCTDynamicTypeRampCaption1): @12,
@(RCTDynamicTypeRampFootnote): @13,
@(RCTDynamicTypeRampSubheadline): @15,
@(RCTDynamicTypeRampCallout): @16,
@(RCTDynamicTypeRampBody): @17,
@(RCTDynamicTypeRampHeadline): @17,
@(RCTDynamicTypeRampTitle3): @20,
@(RCTDynamicTypeRampTitle2): @22,
@(RCTDynamicTypeRampTitle1): @28,
@(RCTDynamicTypeRampLargeTitle): @34,
};
});

NSNumber *baseSize = mapping[@(dynamicTypeRamp)] ?: @17; // Default to body size if we don't recognize the specified ramp
return CGFLOAT_IS_DOUBLE ? [baseSize doubleValue] : [baseSize floatValue];
}
1 change: 1 addition & 0 deletions Libraries/Text/TextNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const textViewConfig = {
numberOfLines: true,
ellipsizeMode: true,
allowFontScaling: true,
dynamicTypeRamp: true,
maxFontSizeMultiplier: true,
disabled: true,
selectable: true,
Expand Down
17 changes: 17 additions & 0 deletions Libraries/Text/TextProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,23 @@ export type TextProps = $ReadOnly<{|
*/
adjustsFontSizeToFit?: ?boolean,

/**
* The Dynamic Text scale ramp to apply to this element on iOS.
*/
dynamicTypeRamp?: ?(
| 'caption2'
| 'caption1'
| 'footnote'
| 'subheadline'
| 'callout'
| 'body'
| 'headline'
| 'title3'
| 'title2'
| 'title1'
| 'largeTitle'
),

/**
* Smallest possible scale a font can reach.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ void TextAttributes::apply(TextAttributes textAttributes) {
allowFontScaling = textAttributes.allowFontScaling.has_value()
? textAttributes.allowFontScaling
: allowFontScaling;
dynamicTypeRamp = textAttributes.dynamicTypeRamp.has_value()
? textAttributes.dynamicTypeRamp
: dynamicTypeRamp;
letterSpacing = !std::isnan(textAttributes.letterSpacing)
? textAttributes.letterSpacing
: letterSpacing;
Expand Down Expand Up @@ -111,6 +114,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
fontStyle,
fontVariant,
allowFontScaling,
dynamicTypeRamp,
alignment,
baseWritingDirection,
lineBreakStrategy,
Expand All @@ -131,6 +135,7 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const {
rhs.fontStyle,
rhs.fontVariant,
rhs.allowFontScaling,
rhs.dynamicTypeRamp,
rhs.alignment,
rhs.baseWritingDirection,
rhs.lineBreakStrategy,
Expand Down Expand Up @@ -186,6 +191,7 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const {
debugStringConvertibleItem("fontStyle", fontStyle),
debugStringConvertibleItem("fontVariant", fontVariant),
debugStringConvertibleItem("allowFontScaling", allowFontScaling),
debugStringConvertibleItem("dynamicTypeRamp", dynamicTypeRamp),
debugStringConvertibleItem("letterSpacing", letterSpacing),

// Paragraph Styles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class TextAttributes : public DebugStringConvertible {
std::optional<FontStyle> fontStyle{};
std::optional<FontVariant> fontVariant{};
std::optional<bool> allowFontScaling{};
std::optional<DynamicTypeRamp> dynamicTypeRamp{};
Float letterSpacing{std::numeric_limits<Float>::quiet_NaN()};
std::optional<TextTransform> textTransform{};

Expand Down
78 changes: 78 additions & 0 deletions ReactCommon/react/renderer/attributedstring/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,84 @@
namespace facebook {
namespace react {

inline std::string toString(const DynamicTypeRamp &dynamicTypeRamp) {
switch (dynamicTypeRamp) {
case DynamicTypeRamp::Caption2:
return "caption2";
case DynamicTypeRamp::Caption1:
return "caption1";
case DynamicTypeRamp::Footnote:
return "footnote";
case DynamicTypeRamp::Subheadline:
return "subheadline";
case DynamicTypeRamp::Callout:
return "callout";
case DynamicTypeRamp::Body:
return "body";
case DynamicTypeRamp::Headline:
return "headline";
case DynamicTypeRamp::Title3:
return "title3";
case DynamicTypeRamp::Title2:
return "title2";
case DynamicTypeRamp::Title1:
return "title1";
case DynamicTypeRamp::LargeTitle:
return "largeTitle";
}

LOG(ERROR) << "Unsupported DynamicTypeRamp value";
react_native_assert(false);

// Sane default in case of parsing errors
return "body";
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
DynamicTypeRamp &result) {
react_native_assert(value.hasType<std::string>());
if (value.hasType<std::string>()) {
auto string = (std::string)value;
if (string == "caption2") {
result = DynamicTypeRamp::Caption2;
} else if (string == "caption1") {
result = DynamicTypeRamp::Caption1;
} else if (string == "footnote") {
result = DynamicTypeRamp::Footnote;
} else if (string == "subheadline") {
result = DynamicTypeRamp::Subheadline;
} else if (string == "callout") {
result = DynamicTypeRamp::Callout;
} else if (string == "body") {
result = DynamicTypeRamp::Body;
} else if (string == "headline") {
result = DynamicTypeRamp::Headline;
} else if (string == "title3") {
result = DynamicTypeRamp::Title3;
} else if (string == "title2") {
result = DynamicTypeRamp::Title2;
} else if (string == "title1") {
result = DynamicTypeRamp::Title1;
} else if (string == "largeTitle") {
result = DynamicTypeRamp::LargeTitle;
} else {
// sane default
LOG(ERROR) << "Unsupported DynamicTypeRamp value: " << string;
react_native_assert(false);
result = DynamicTypeRamp::Body;
}
return;
}

LOG(ERROR) << "Unsupported DynamicTypeRamp type";
react_native_assert(false);

// Sane default in case of parsing errors
result = DynamicTypeRamp::Body;
}

inline std::string toString(const EllipsizeMode &ellipsisMode) {
switch (ellipsisMode) {
case EllipsizeMode::Clip:
Expand Down
21 changes: 21 additions & 0 deletions ReactCommon/react/renderer/attributedstring/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ enum class FontVariant : int {
ProportionalNums = 1 << 5
};

enum class DynamicTypeRamp {
Caption2,
Caption1,
Footnote,
Subheadline,
Callout,
Body,
Headline,
Title3,
Title2,
Title1,
LargeTitle
};

enum class EllipsizeMode {
Clip, // Do not add ellipsize, simply clip.
Head, // Truncate at head of line: "...wxyz".
Expand Down Expand Up @@ -190,6 +204,13 @@ struct hash<facebook::react::FontWeight> {
}
};

template <>
struct hash<facebook::react::DynamicTypeRamp> {
size_t operator()(const facebook::react::DynamicTypeRamp &v) const {
return hash<int>()(static_cast<int>(v));
}
};

template <>
struct hash<facebook::react::EllipsizeMode> {
size_t operator()(const facebook::react::EllipsizeMode &v) const {
Expand Down
6 changes: 6 additions & 0 deletions ReactCommon/react/renderer/components/text/BaseTextProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ static TextAttributes convertRawProp(
"allowFontScaling",
sourceTextAttributes.allowFontScaling,
defaultTextAttributes.allowFontScaling);
textAttributes.dynamicTypeRamp = convertRawProp(
context,
rawProps,
"dynamicTypeRamp",
sourceTextAttributes.dynamicTypeRamp,
defaultTextAttributes.dynamicTypeRamp);
textAttributes.letterSpacing = convertRawProp(
context,
rawProps,
Expand Down
Loading