mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-10 12:21:16 +02:00
303 lines
14 KiB
Text
303 lines
14 KiB
Text
#include "T3MarkdownTextShadowNode.h"
|
|
#include "T3MarkdownTextRunShadowNode.h"
|
|
#import "T3ContextChip.h"
|
|
#include <react/renderer/components/view/ViewShadowNode.h>
|
|
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace facebook::react {
|
|
|
|
static constexpr Float ParagraphStyleEncodingOffset = 1000;
|
|
static constexpr auto FileAttachmentNativeIdPrefix = "t3-file:";
|
|
static constexpr auto SkillAttachmentNativeIdPrefix = "t3-skill:";
|
|
static constexpr auto LinkAttachmentNativeIdPrefix = "t3-link:";
|
|
|
|
static void applyParagraphStyles(
|
|
NSMutableAttributedString *attributedString,
|
|
const std::vector<T3MarkdownTextParagraphStyleRange> &styleRanges)
|
|
{
|
|
for (const auto &styleRange : styleRanges) {
|
|
if (styleRange.length == 0 || styleRange.location >= attributedString.length) {
|
|
continue;
|
|
}
|
|
|
|
const NSRange markerRange = NSMakeRange(
|
|
styleRange.location,
|
|
MIN(styleRange.length, attributedString.length - styleRange.location));
|
|
const NSRange paragraphRange = [attributedString.string paragraphRangeForRange:markerRange];
|
|
const NSParagraphStyle *existingStyle =
|
|
[attributedString attribute:NSParagraphStyleAttributeName
|
|
atIndex:paragraphRange.location
|
|
effectiveRange:nil];
|
|
NSMutableParagraphStyle *paragraphStyle =
|
|
existingStyle ? [existingStyle mutableCopy] : [NSMutableParagraphStyle new];
|
|
paragraphStyle.firstLineHeadIndent = styleRange.firstLineHeadIndent;
|
|
paragraphStyle.headIndent = styleRange.headIndent;
|
|
paragraphStyle.paragraphSpacing = styleRange.paragraphSpacing;
|
|
paragraphStyle.tabStops = @[
|
|
[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft
|
|
location:styleRange.headIndent
|
|
options:@{}]
|
|
];
|
|
paragraphStyle.defaultTabInterval = styleRange.headIndent;
|
|
[attributedString addAttribute:NSParagraphStyleAttributeName
|
|
value:paragraphStyle
|
|
range:paragraphRange];
|
|
}
|
|
}
|
|
|
|
static void applyAttachments(
|
|
NSMutableAttributedString *attributedString,
|
|
const std::vector<T3MarkdownTextAttachmentRange> &attachmentRanges)
|
|
{
|
|
for (const auto &attachmentRange : attachmentRanges) {
|
|
if (attachmentRange.length == 0 || attachmentRange.location >= attributedString.length) {
|
|
continue;
|
|
}
|
|
|
|
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
|
|
attachment.image = [[UIImage alloc] init];
|
|
const CGFloat attachmentSize = T3MarkdownTextAttachmentSize(attachmentRange);
|
|
attachment.bounds = CGRectMake(
|
|
0,
|
|
T3MarkdownTextAttachmentBaselineOffset(attachmentRange),
|
|
attachmentSize,
|
|
attachmentSize);
|
|
NSDictionary *runAttributes =
|
|
[attributedString attributesAtIndex:attachmentRange.location effectiveRange:nil];
|
|
if (attachmentRange.chipWidth > 0) {
|
|
attachment.bounds = T3ContextChipBounds(
|
|
runAttributes[NSFontAttributeName],
|
|
CGSizeMake(attachmentRange.chipWidth, attachmentRange.chipHeight));
|
|
}
|
|
const NSRange range = NSMakeRange(
|
|
attachmentRange.location,
|
|
MIN(attachmentRange.length, attributedString.length - attachmentRange.location));
|
|
[attributedString replaceCharactersInRange:range
|
|
withAttributedString:T3MarkdownTextAttachmentString(attachment, runAttributes)];
|
|
}
|
|
}
|
|
|
|
T3MarkdownTextShadowNode::T3MarkdownTextShadowNode(
|
|
const ShadowNode& sourceShadowNode,
|
|
const ShadowNodeFragment& fragment
|
|
) : ConcreteViewShadowNode(sourceShadowNode, fragment) {
|
|
};
|
|
|
|
Size T3MarkdownTextShadowNode::measureContent(
|
|
const LayoutContext& layoutContext,
|
|
const LayoutConstraints& layoutConstraints) const {
|
|
const auto &baseProps = getConcreteProps();
|
|
|
|
auto baseTextAttributes = TextAttributes::defaultTextAttributes();
|
|
baseTextAttributes.backgroundColor = baseProps.backgroundColor;
|
|
baseTextAttributes.allowFontScaling = baseProps.allowFontScaling;
|
|
|
|
Float fontSizeMultiplier = 1.0;
|
|
if (baseTextAttributes.allowFontScaling) {
|
|
fontSizeMultiplier = layoutContext.fontSizeMultiplier;
|
|
}
|
|
|
|
auto baseAttributedString = AttributedString{};
|
|
auto paragraphStyleRanges = std::vector<T3MarkdownTextParagraphStyleRange>{};
|
|
auto attachmentRanges = std::vector<T3MarkdownTextAttachmentRange>{};
|
|
size_t utf16Offset = 0;
|
|
const auto &children = getChildren();
|
|
for (size_t i = 0; i < children.size(); i++) {
|
|
const auto child = children[i].get();
|
|
if (auto textViewChild = dynamic_cast<const T3MarkdownTextRunShadowNode *>(child)) {
|
|
auto &props = textViewChild->getConcreteProps();
|
|
auto fragment = AttributedString::Fragment{};
|
|
auto textAttributes = TextAttributes::defaultTextAttributes();
|
|
|
|
textAttributes.allowFontScaling = baseProps.allowFontScaling;
|
|
textAttributes.backgroundColor = props.backgroundColor;
|
|
textAttributes.fontSize = props.fontSize * fontSizeMultiplier;
|
|
textAttributes.lineHeight = props.lineHeight * fontSizeMultiplier;
|
|
textAttributes.foregroundColor = props.color;
|
|
const bool hasParagraphStyle = props.shadowRadius >= ParagraphStyleEncodingOffset;
|
|
if (!hasParagraphStyle) {
|
|
textAttributes.textShadowColor = props.shadowColor;
|
|
textAttributes.textShadowOffset = props.shadowOffset;
|
|
textAttributes.textShadowRadius = props.shadowRadius;
|
|
}
|
|
textAttributes.letterSpacing = props.letterSpacing;
|
|
textAttributes.textDecorationColor = props.textDecorationColor;
|
|
textAttributes.fontFamily = props.fontFamily;
|
|
|
|
if (props.fontStyle == T3MarkdownTextRunFontStyle::Italic) {
|
|
textAttributes.fontStyle = FontStyle::Italic;
|
|
} else {
|
|
textAttributes.fontStyle = FontStyle::Normal;
|
|
}
|
|
|
|
if (props.fontWeight == T3MarkdownTextRunFontWeight::Bold) {
|
|
textAttributes.fontWeight = FontWeight::Bold;
|
|
} else if (props.fontWeight == T3MarkdownTextRunFontWeight::UltraLight) {
|
|
textAttributes.fontWeight = FontWeight::UltraLight;
|
|
} else if (props.fontWeight == T3MarkdownTextRunFontWeight::Light) {
|
|
textAttributes.fontWeight = FontWeight::Light;
|
|
} else if (props.fontWeight == T3MarkdownTextRunFontWeight::Medium) {
|
|
textAttributes.fontWeight = FontWeight::Medium;
|
|
} else if (props.fontWeight == T3MarkdownTextRunFontWeight::Semibold) {
|
|
textAttributes.fontWeight = FontWeight::Semibold;
|
|
} else if (props.fontWeight == T3MarkdownTextRunFontWeight::Heavy) {
|
|
textAttributes.fontWeight = FontWeight::Heavy;
|
|
} else {
|
|
textAttributes.fontWeight = FontWeight::Regular;
|
|
}
|
|
|
|
if (props.textDecorationLine == T3MarkdownTextRunTextDecorationLine::LineThrough) {
|
|
textAttributes.textDecorationLineType = TextDecorationLineType::Strikethrough;
|
|
} else if (props.textDecorationLine == T3MarkdownTextRunTextDecorationLine::Underline) {
|
|
textAttributes.textDecorationLineType = TextDecorationLineType::Underline;
|
|
} else {
|
|
textAttributes.textDecorationLineType = TextDecorationLineType::None;
|
|
}
|
|
|
|
if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Solid) {
|
|
textAttributes.textDecorationStyle = TextDecorationStyle::Solid;
|
|
} else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Dotted) {
|
|
textAttributes.textDecorationStyle = TextDecorationStyle::Dotted;
|
|
} else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Dashed) {
|
|
textAttributes.textDecorationStyle = TextDecorationStyle::Dashed;
|
|
} else if (props.textDecorationStyle == T3MarkdownTextRunTextDecorationStyle::Double) {
|
|
textAttributes.textDecorationStyle = TextDecorationStyle::Double;
|
|
}
|
|
|
|
if (props.textAlign == T3MarkdownTextRunTextAlign::Left) {
|
|
textAttributes.alignment = TextAlignment::Left;
|
|
} else if (props.textAlign == T3MarkdownTextRunTextAlign::Right) {
|
|
textAttributes.alignment = TextAlignment::Right;
|
|
} else if (props.textAlign == T3MarkdownTextRunTextAlign::Center) {
|
|
textAttributes.alignment = TextAlignment::Center;
|
|
} else if (props.textAlign == T3MarkdownTextRunTextAlign::Justify) {
|
|
textAttributes.alignment = TextAlignment::Justified;
|
|
} else if (props.textAlign == T3MarkdownTextRunTextAlign::Auto) {
|
|
textAttributes.alignment = TextAlignment::Natural;
|
|
}
|
|
|
|
textAttributes.backgroundColor = props.backgroundColor;
|
|
|
|
fragment.string = props.text;
|
|
fragment.textAttributes = textAttributes;
|
|
|
|
NSString *fragmentText = [NSString stringWithUTF8String:props.text.c_str()];
|
|
const size_t fragmentLength = fragmentText.length;
|
|
if (hasParagraphStyle) {
|
|
paragraphStyleRanges.push_back(T3MarkdownTextParagraphStyleRange{
|
|
utf16Offset,
|
|
fragmentLength,
|
|
props.shadowOffset.width,
|
|
props.shadowOffset.height,
|
|
props.shadowRadius - ParagraphStyleEncodingOffset,
|
|
});
|
|
}
|
|
if (props.nativeId.rfind("t3-chip:", 0) == 0 && fragmentLength > 0) {
|
|
const std::string uri = props.nativeId.substr(3);
|
|
NSMutableDictionary *payload =
|
|
[T3ContextChipPayload([NSString stringWithUTF8String:uri.c_str()]) mutableCopy];
|
|
// Chips must scale with the paragraph or smaller Dynamic Type sizes clip them.
|
|
// Store the scaled payload so measurement and the rendered bitmap use the same font.
|
|
payload[@"fontSizeMultiplier"] = @(fontSizeMultiplier);
|
|
NSData *data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
|
|
NSString *scaledUri = [@"chip:" stringByAppendingString:
|
|
[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
|
|
const CGFloat maxWidth = std::isfinite(layoutConstraints.maximumSize.width)
|
|
? layoutConstraints.maximumSize.width : 320;
|
|
const CGSize size = T3ContextChipSize(payload, maxWidth);
|
|
attachmentRanges.push_back(T3MarkdownTextAttachmentRange{
|
|
utf16Offset, 1, std::string(scaledUri.UTF8String), false,
|
|
static_cast<Float>(size.width), static_cast<Float>(size.height),
|
|
});
|
|
} else if (props.nativeId.rfind(FileAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) {
|
|
attachmentRanges.push_back(T3MarkdownTextAttachmentRange{
|
|
utf16Offset,
|
|
1,
|
|
props.nativeId.substr(std::char_traits<char>::length(FileAttachmentNativeIdPrefix)),
|
|
false,
|
|
});
|
|
} else if (
|
|
props.nativeId.rfind(SkillAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) {
|
|
attachmentRanges.push_back(T3MarkdownTextAttachmentRange{
|
|
utf16Offset,
|
|
1,
|
|
props.nativeId.substr(
|
|
std::char_traits<char>::length(SkillAttachmentNativeIdPrefix)),
|
|
false,
|
|
});
|
|
} else if (
|
|
props.nativeId.rfind(LinkAttachmentNativeIdPrefix, 0) == 0 && fragmentLength > 0) {
|
|
attachmentRanges.push_back(T3MarkdownTextAttachmentRange{
|
|
utf16Offset,
|
|
1,
|
|
props.nativeId.substr(std::char_traits<char>::length(LinkAttachmentNativeIdPrefix)),
|
|
true,
|
|
});
|
|
}
|
|
utf16Offset += fragmentLength;
|
|
baseAttributedString.appendFragment(std::move(fragment));
|
|
}
|
|
}
|
|
|
|
_attributedString = baseAttributedString;
|
|
_paragraphStyleRanges = paragraphStyleRanges;
|
|
_attachmentRanges = attachmentRanges;
|
|
|
|
NSMutableAttributedString *convertedAttributedString =
|
|
[RCTNSAttributedStringFromAttributedString(baseAttributedString) mutableCopy];
|
|
applyParagraphStyles(convertedAttributedString, paragraphStyleRanges);
|
|
applyAttachments(convertedAttributedString, attachmentRanges);
|
|
// TextKit stacks a paragraph's extra line height above the glyphs. React Native's own
|
|
// layout manager centres them with a baseline offset; do the same, after attachments
|
|
// so chips shift with the words.
|
|
RCTApplyBaselineOffset(convertedAttributedString);
|
|
|
|
const CGFloat maximumWidth = std::isfinite(layoutConstraints.maximumSize.width)
|
|
? layoutConstraints.maximumSize.width
|
|
: CGFLOAT_MAX;
|
|
NSTextStorage *textStorage =
|
|
[[NSTextStorage alloc] initWithAttributedString:convertedAttributedString];
|
|
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
|
|
layoutManager.usesFontLeading = NO;
|
|
NSTextContainer *textContainer =
|
|
[[NSTextContainer alloc] initWithSize:CGSizeMake(maximumWidth, CGFLOAT_MAX)];
|
|
textContainer.lineFragmentPadding = 0;
|
|
textContainer.maximumNumberOfLines = baseProps.numberOfLines;
|
|
if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Head) {
|
|
textContainer.lineBreakMode = NSLineBreakByTruncatingHead;
|
|
} else if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Middle) {
|
|
textContainer.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
|
} else if (baseProps.ellipsizeMode == T3MarkdownTextEllipsizeMode::Tail) {
|
|
textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
|
|
} else {
|
|
textContainer.lineBreakMode = NSLineBreakByClipping;
|
|
}
|
|
[layoutManager addTextContainer:textContainer];
|
|
[textStorage addLayoutManager:layoutManager];
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
const CGRect usedRect = [layoutManager usedRectForTextContainer:textContainer];
|
|
|
|
return {
|
|
std::clamp(
|
|
static_cast<Float>(std::ceil(usedRect.size.width)),
|
|
layoutConstraints.minimumSize.width,
|
|
layoutConstraints.maximumSize.width),
|
|
std::clamp(
|
|
static_cast<Float>(std::ceil(usedRect.size.height)),
|
|
layoutConstraints.minimumSize.height,
|
|
layoutConstraints.maximumSize.height),
|
|
};
|
|
}
|
|
|
|
void T3MarkdownTextShadowNode::layout(LayoutContext layoutContext) {
|
|
ensureUnsealed();
|
|
setStateData(T3MarkdownTextStateReal{
|
|
_attributedString,
|
|
_paragraphStyleRanges,
|
|
_attachmentRanges,
|
|
});
|
|
}
|
|
}
|