/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTAttributedTextUtils.h" #include #include #include #include #include using namespace facebook::react; @implementation RCTWeakEventEmitterWrapper { std::weak_ptr _weakEventEmitter; } - (void)setEventEmitter:(SharedEventEmitter)eventEmitter { _weakEventEmitter = eventEmitter; } - (SharedEventEmitter)eventEmitter { return _weakEventEmitter.lock(); } - (void)dealloc { _weakEventEmitter.reset(); } @end inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight) { assert(fontWeight > 50); assert(fontWeight < 950); static UIFontWeight weights[] = { /* ~100 */ UIFontWeightUltraLight, /* ~200 */ UIFontWeightThin, /* ~300 */ UIFontWeightLight, /* ~400 */ UIFontWeightRegular, /* ~500 */ UIFontWeightMedium, /* ~600 */ UIFontWeightSemibold, /* ~700 */ UIFontWeightBold, /* ~800 */ UIFontWeightHeavy, /* ~900 */ UIFontWeightBlack}; // The expression is designed to convert something like 760 or 830 to 7. return weights[(fontWeight + 50) / 100 - 1]; } inline static UIFont *RCTEffectiveFontFromTextAttributes(const TextAttributes &textAttributes) { NSString *fontFamily = [NSString stringWithCString:textAttributes.fontFamily.c_str() encoding:NSUTF8StringEncoding]; RCTFontProperties fontProperties; fontProperties.family = fontFamily; fontProperties.size = textAttributes.fontSize; fontProperties.style = textAttributes.fontStyle.hasValue() ? RCTFontStyleFromFontStyle(textAttributes.fontStyle.value()) : RCTFontStyleUndefined; fontProperties.variant = textAttributes.fontVariant.hasValue() ? RCTFontVariantFromFontVariant(textAttributes.fontVariant.value()) : RCTFontVariantUndefined; fontProperties.weight = textAttributes.fontWeight.hasValue() ? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value()) : NAN; fontProperties.sizeMultiplier = textAttributes.fontSizeMultiplier; return RCTFontWithFontProperties(fontProperties); } inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes) { return textAttributes.allowFontScaling.value_or(true) && !isnan(textAttributes.fontSizeMultiplier) ? textAttributes.fontSizeMultiplier : 1.0; } inline static UIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes) { UIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [UIColor blackColor]; if (!isnan(textAttributes.opacity)) { effectiveForegroundColor = [effectiveForegroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveForegroundColor.CGColor) * textAttributes.opacity]; } return effectiveForegroundColor; } inline static UIColor *RCTEffectiveBackgroundColorFromTextAttributes(const TextAttributes &textAttributes) { UIColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor); if (effectiveBackgroundColor && !isnan(textAttributes.opacity)) { effectiveBackgroundColor = [effectiveBackgroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * textAttributes.opacity]; } return effectiveBackgroundColor ?: [UIColor clearColor]; } NSDictionary *RCTNSTextAttributesFromTextAttributes(TextAttributes const &textAttributes) { NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:10]; // Font UIFont *font = RCTEffectiveFontFromTextAttributes(textAttributes); if (font) { attributes[NSFontAttributeName] = font; } // Colors UIColor *effectiveForegroundColor = RCTEffectiveForegroundColorFromTextAttributes(textAttributes); if (textAttributes.foregroundColor || !isnan(textAttributes.opacity)) { attributes[NSForegroundColorAttributeName] = effectiveForegroundColor; } if (textAttributes.backgroundColor || !isnan(textAttributes.opacity)) { attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); } // Kerning if (!isnan(textAttributes.letterSpacing)) { attributes[NSKernAttributeName] = @(textAttributes.letterSpacing); } // Paragraph Style NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; BOOL isParagraphStyleUsed = NO; if (textAttributes.alignment.hasValue()) { TextAlignment textAlignment = textAttributes.alignment.value_or(TextAlignment::Natural); if (textAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight) == LayoutDirection::RightToLeft) { if (textAlignment == TextAlignment::Right) { textAlignment = TextAlignment::Left; } else if (textAlignment == TextAlignment::Left) { textAlignment = TextAlignment::Right; } } paragraphStyle.alignment = RCTNSTextAlignmentFromTextAlignment(textAlignment); isParagraphStyleUsed = YES; } if (textAttributes.baseWritingDirection.hasValue()) { paragraphStyle.baseWritingDirection = RCTNSWritingDirectionFromWritingDirection(textAttributes.baseWritingDirection.value()); isParagraphStyleUsed = YES; } if (!isnan(textAttributes.lineHeight)) { CGFloat lineHeight = textAttributes.lineHeight * RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes); paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; isParagraphStyleUsed = YES; } if (isParagraphStyleUsed) { attributes[NSParagraphStyleAttributeName] = paragraphStyle; } // Decoration if (textAttributes.textDecorationLineType.value_or(TextDecorationLineType::None) != TextDecorationLineType::None) { auto textDecorationLineType = textAttributes.textDecorationLineType.value(); NSUnderlineStyle style = RCTNSUnderlineStyleFromStyleAndPattern( textAttributes.textDecorationLineStyle.value_or(TextDecorationLineStyle::Single), textAttributes.textDecorationLinePattern.value_or(TextDecorationLinePattern::Solid)); UIColor *textDecorationColor = RCTUIColorFromSharedColor(textAttributes.textDecorationColor); // Underline if (textDecorationLineType == TextDecorationLineType::Underline || textDecorationLineType == TextDecorationLineType::UnderlineStrikethrough) { attributes[NSUnderlineStyleAttributeName] = @(style); if (textDecorationColor) { attributes[NSUnderlineColorAttributeName] = textDecorationColor; } } // Strikethrough if (textDecorationLineType == TextDecorationLineType::Strikethrough || textDecorationLineType == TextDecorationLineType::UnderlineStrikethrough) { attributes[NSStrikethroughStyleAttributeName] = @(style); if (textDecorationColor) { attributes[NSStrikethroughColorAttributeName] = textDecorationColor; } } } // Shadow if (textAttributes.textShadowOffset.hasValue()) { auto textShadowOffset = textAttributes.textShadowOffset.value(); NSShadow *shadow = [NSShadow new]; shadow.shadowOffset = CGSize{textShadowOffset.width, textShadowOffset.height}; shadow.shadowBlurRadius = textAttributes.textShadowRadius; shadow.shadowColor = RCTUIColorFromSharedColor(textAttributes.textShadowColor); attributes[NSShadowAttributeName] = shadow; } // Special if (textAttributes.isHighlighted) { attributes[RCTAttributedStringIsHighlightedAttributeName] = @YES; } if (textAttributes.accessibilityRole.hasValue()) { auto accessibilityRole = textAttributes.accessibilityRole.value(); switch (accessibilityRole) { case AccessibilityRole::None: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("none"); break; case AccessibilityRole::Button: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("button"); break; case AccessibilityRole::Link: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("link"); break; case AccessibilityRole::Search: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("search"); break; case AccessibilityRole::Image: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("image"); break; case AccessibilityRole::Imagebutton: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton"); break; case AccessibilityRole::Keyboardkey: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("keyboardkey"); break; case AccessibilityRole::Text: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("text"); break; case AccessibilityRole::Adjustable: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("adjustable"); break; case AccessibilityRole::Summary: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary"); break; case AccessibilityRole::Header: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("header"); break; case AccessibilityRole::Alert: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("alert"); break; case AccessibilityRole::Checkbox: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("checkbox"); break; case AccessibilityRole::Combobox: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("combobox"); break; case AccessibilityRole::Menu: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menu"); break; case AccessibilityRole::Menubar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menubar"); break; case AccessibilityRole::Menuitem: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menuitem"); break; case AccessibilityRole::Progressbar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("progressbar"); break; case AccessibilityRole::Radio: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radio"); break; case AccessibilityRole::Radiogroup: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radiogroup"); break; case AccessibilityRole::Scrollbar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("scrollbar"); break; case AccessibilityRole::Spinbutton: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("spinbutton"); break; case AccessibilityRole::Switch: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("switch"); break; case AccessibilityRole::Tab: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tab"); break; case AccessibilityRole::TabBar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tabbar"); break; case AccessibilityRole::Tablist: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tablist"); break; case AccessibilityRole::Timer: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("timer"); break; case AccessibilityRole::Toolbar: attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("toolbar"); break; }; } return [attributes copy]; } NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString) { static UIImage *placeholderImage; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ placeholderImage = [[UIImage alloc] init]; }); NSMutableAttributedString *nsAttributedString = [[NSMutableAttributedString alloc] init]; [nsAttributedString beginEditing]; for (auto fragment : attributedString.getFragments()) { NSMutableAttributedString *nsAttributedStringFragment; if (fragment.isAttachment()) { auto layoutMetrics = fragment.parentShadowView.layoutMetrics; CGRect bounds = { .origin = {.x = layoutMetrics.frame.origin.x, .y = layoutMetrics.frame.origin.y}, .size = {.width = layoutMetrics.frame.size.width, .height = layoutMetrics.frame.size.height}}; NSTextAttachment *attachment = [NSTextAttachment new]; attachment.image = placeholderImage; attachment.bounds = bounds; nsAttributedStringFragment = [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy]; } else { NSString *string = [NSString stringWithCString:fragment.string.c_str() encoding:NSUTF8StringEncoding]; nsAttributedStringFragment = [[NSMutableAttributedString alloc] initWithString:string attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)]; } if (fragment.parentShadowView.componentHandle) { RCTWeakEventEmitterWrapper *eventEmitterWrapper = [RCTWeakEventEmitterWrapper new]; eventEmitterWrapper.eventEmitter = fragment.parentShadowView.eventEmitter; NSDictionary *additionalTextAttributes = @{RCTAttributedStringEventEmitterKey : eventEmitterWrapper}; [nsAttributedStringFragment addAttributes:additionalTextAttributes range:NSMakeRange(0, nsAttributedStringFragment.length)]; } [nsAttributedString appendAttributedString:nsAttributedStringFragment]; } [nsAttributedString endEditing]; return nsAttributedString; } NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(AttributedStringBox const &attributedStringBox) { switch (attributedStringBox.getMode()) { case AttributedStringBox::Mode::Value: return RCTNSAttributedStringFromAttributedString(attributedStringBox.getValue()); case AttributedStringBox::Mode::OpaquePointer: return (NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer()); } } AttributedStringBox RCTAttributedStringBoxFromNSAttributedString(NSAttributedString *nsAttributedString) { return nsAttributedString.length ? AttributedStringBox{wrapManagedObject(nsAttributedString)} : AttributedStringBox{}; }