diff --git a/.maestro/enrichedInput/screenshots/ios/inline_styles_merge.png b/.maestro/enrichedInput/screenshots/ios/inline_styles_merge.png index a52dbeef6..7a978bfac 100644 Binary files a/.maestro/enrichedInput/screenshots/ios/inline_styles_merge.png and b/.maestro/enrichedInput/screenshots/ios/inline_styles_merge.png differ diff --git a/.maestro/enrichedInput/screenshots/ios/inline_styles_removal.png b/.maestro/enrichedInput/screenshots/ios/inline_styles_removal.png index a7b19f9c8..76b90d4be 100644 Binary files a/.maestro/enrichedInput/screenshots/ios/inline_styles_removal.png and b/.maestro/enrichedInput/screenshots/ios/inline_styles_removal.png differ diff --git a/ios/extensions/ArrayExtension.h b/ios/extensions/ArrayExtension.h new file mode 100644 index 000000000..f41b79ed8 --- /dev/null +++ b/ios/extensions/ArrayExtension.h @@ -0,0 +1,6 @@ +#import +#pragma once + +@interface NSArray (ArrayExtension) +- (NSArray *)sortedArrayBySortKey:(NSInteger (^)(id item))sortKeyForItem; +@end diff --git a/ios/extensions/ArrayExtension.mm b/ios/extensions/ArrayExtension.mm new file mode 100644 index 000000000..79e2936ad --- /dev/null +++ b/ios/extensions/ArrayExtension.mm @@ -0,0 +1,18 @@ +#import "ArrayExtension.h" + +@implementation NSArray (ArrayExtension) + +- (NSArray *)sortedArrayBySortKey:(NSInteger (^)(id item))sortKeyForItem { + return [self sortedArrayWithOptions:NSSortStable + usingComparator:^NSComparisonResult(id a, id b) { + NSInteger aKey = sortKeyForItem(a); + NSInteger bKey = sortKeyForItem(b); + if (aKey == bKey) { + return NSOrderedSame; + } + return aKey < bKey ? NSOrderedAscending + : NSOrderedDescending; + }]; +} + +@end diff --git a/ios/extensions/FontExtension.h b/ios/extensions/FontExtension.h index 3461ff302..1e00d586c 100644 --- a/ios/extensions/FontExtension.h +++ b/ios/extensions/FontExtension.h @@ -8,4 +8,5 @@ - (UIFont *)setItalic; - (UIFont *)withFontTraits:(UIFont *)from; - (UIFont *)setSize:(CGFloat)size; +- (BOOL)coversCharacters:(const unichar *)chars count:(CFIndex)count; @end diff --git a/ios/extensions/FontExtension.mm b/ios/extensions/FontExtension.mm index 34ca81696..b0228bcda 100644 --- a/ios/extensions/FontExtension.mm +++ b/ios/extensions/FontExtension.mm @@ -1,4 +1,5 @@ #import "FontExtension.h" +#import #import @implementation UIFont (FontExtension) @@ -40,8 +41,6 @@ - (UIFont *)setItalic { if (fontDescriptor != nullptr) { return [UIFont fontWithDescriptor:fontDescriptor size:0]; } else { - RCTLogWarn( - @"[EnrichedTextInput]: Couldn't apply italic trait to the font."); return self; } } @@ -69,4 +68,10 @@ - (UIFont *)setSize:(CGFloat)size { } } +- (BOOL)coversCharacters:(const unichar *)chars count:(CFIndex)count { + CGGlyph glyphs[2] = {0, 0}; + return CTFontGetGlyphsForCharacters((__bridge CTFontRef)self, chars, glyphs, + count); +} + @end diff --git a/ios/inputAttributesManager/InputAttributesManager.mm b/ios/inputAttributesManager/InputAttributesManager.mm index f299b5398..cd768339a 100644 --- a/ios/inputAttributesManager/InputAttributesManager.mm +++ b/ios/inputAttributesManager/InputAttributesManager.mm @@ -1,10 +1,12 @@ #import "InputAttributesManager.h" #import "AlignmentUtils.h" +#import "ArrayExtension.h" #import "AttributeEntry.h" #import "EnrichedTextInputView.h" #import "ParagraphAttributesUtils.h" #import "RangeUtils.h" #import "StyleHeaders.h" +#import "StyleUtils.h" #import "ZeroWidthSpaceUtils.h" @implementation InputAttributesManager { @@ -99,15 +101,11 @@ - (void)handleDirtyRangesStyling { // Sort style types so paragraph styles come first. Their broad visual // attributes (e.g. foreground color, font) are laid down before inline - // styles override them on their specific sub-ranges. + // styles override them on their specific sub-ranges. Inline styles among + // themselves follow their stylePriority. NSArray *sortedStyleTypes = [presentStyles.allKeys - sortedArrayUsingComparator:^NSComparisonResult(NSNumber *a, - NSNumber *b) { - BOOL aPara = [_input->stylesDict[a] isParagraph]; - BOOL bPara = [_input->stylesDict[b] isParagraph]; - if (aPara == bPara) - return NSOrderedSame; - return aPara ? NSOrderedAscending : NSOrderedDescending; + sortedArrayBySortKey:^NSInteger(NSNumber *styleType) { + return [_input->stylesDict[styleType] stylePriority]; }]; // re-apply meta-attributes and apply visual styling following the saved diff --git a/ios/interfaces/StyleBase.h b/ios/interfaces/StyleBase.h index 41a3d27de..343f88de3 100644 --- a/ios/interfaces/StyleBase.h +++ b/ios/interfaces/StyleBase.h @@ -14,6 +14,7 @@ - (BOOL)isParagraph; - (BOOL)needsZWS; - (BOOL)appliesStylingToTyping; +- (NSInteger)stylePriority; - (instancetype)initWithHost:(id)host; - (NSRange)actualUsedRange:(NSRange)range; - (void)toggle:(NSRange)range; diff --git a/ios/interfaces/StyleBase.mm b/ios/interfaces/StyleBase.mm index f43c53224..ef78d4d27 100644 --- a/ios/interfaces/StyleBase.mm +++ b/ios/interfaces/StyleBase.mm @@ -45,6 +45,11 @@ - (BOOL)appliesStylingToTyping { return NO; } +// determines the order in which the styles are applied +- (NSInteger)stylePriority { + return [self isParagraph] ? 0 : 2; +} + - (instancetype)initWithHost:(id)host { self = [super init]; _host = host; diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm index f2a161141..d82f44388 100644 --- a/ios/styles/ItalicStyle.mm +++ b/ios/styles/ItalicStyle.mm @@ -1,5 +1,5 @@ #import "EnrichedTextInputView.h" -#import "FontExtension.h" +#import "ItalicUtils.h" #import "StyleHeaders.h" @implementation ItalicStyle : StyleBase @@ -16,21 +16,33 @@ - (BOOL)isParagraph { return NO; } +// some styles might apply a new font (inline code), so we need to apply +// the italic last, that way we know if the used font supports italics +// or we need to apply a slant +- (NSInteger)stylePriority { + return 3; +} + - (void)applyStyling:(NSRange)range { + if (self.host.textView.textStorage == nullptr || range.length == 0 || + NSMaxRange(range) > self.host.textView.textStorage.length) { + return; + } + + // we process each present font [self.host.textView.textStorage enumerateAttribute:NSFontAttributeName inRange:range options:0 - usingBlock:^(id _Nullable value, NSRange range, + usingBlock:^(id _Nullable value, NSRange fontRange, BOOL *_Nonnull stop) { UIFont *font = (UIFont *)value; - if (font != nullptr) { - UIFont *newFont = [font setItalic]; - [self.host.textView.textStorage - addAttribute:NSFontAttributeName - value:newFont - range:range]; + if (font == nullptr) { + return; } + [ItalicUtils applyItalicForFont:font + inTextStorage:self.host.textView.textStorage + inRange:fontRange]; }]; } diff --git a/ios/textHtmlParser/TextHtmlParser.mm b/ios/textHtmlParser/TextHtmlParser.mm index 3899fd2d8..c7810d276 100644 --- a/ios/textHtmlParser/TextHtmlParser.mm +++ b/ios/textHtmlParser/TextHtmlParser.mm @@ -1,5 +1,6 @@ #import "TextHtmlParser.h" #import "AlignmentEntry.h" +#import "ArrayExtension.h" #import "EnrichedTextView.h" #import "HtmlParser.h" #import "LinkData.h" @@ -153,8 +154,14 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles { } } + // Respect the styling priority + NSArray *sortedInlineApply = + [pendingInlineApply sortedArrayBySortKey:^NSInteger(NSArray *entry) { + return [((StyleBase *)entry[0]) stylePriority]; + }]; + // Apply visual styling for inline styles - for (NSArray *entry in pendingInlineApply) { + for (NSArray *entry in sortedInlineApply) { StyleBase *style = entry[0]; NSRange adjustedStyleRange = [((NSValue *)entry[1]) rangeValue]; [style applyStyling:adjustedStyleRange]; diff --git a/ios/utils/ItalicUtils.h b/ios/utils/ItalicUtils.h new file mode 100644 index 000000000..f1fa15113 --- /dev/null +++ b/ios/utils/ItalicUtils.h @@ -0,0 +1,20 @@ +#import +#pragma once + +typedef NS_ENUM(NSInteger, ItalicKind) { + // character must not be slanted at all (whitespace, control characters, + // text attachments) + ItalicKindNone, + // font has a real italic glyph for the character + ItalicKindFont, + // no italic glyph available, the slant has to be used + ItalicKindOblique, +}; + +@interface ItalicUtils : NSObject + ++ (void)applyItalicForFont:(UIFont *)font + inTextStorage:(NSTextStorage *)textStorage + inRange:(NSRange)range; + +@end diff --git a/ios/utils/ItalicUtils.mm b/ios/utils/ItalicUtils.mm new file mode 100644 index 000000000..d6fbcec5c --- /dev/null +++ b/ios/utils/ItalicUtils.mm @@ -0,0 +1,151 @@ +#import "ItalicUtils.h" +#import "FontExtension.h" + +// slant used when a font has no italic face +static const CGFloat kObliquenessFallback = 0.2; + +@interface ItalicUtils () + ++ (NSCharacterSet *)nonNeutralCharacters; + ++ (ItalicKind)kindForCluster:(NSString *)cluster + font:(UIFont *)font + italicFont:(UIFont *)italicFont + hasItalicFace:(BOOL)hasItalicFace; + ++ (void)applyKind:(ItalicKind)kind + toSegment:(NSRange)segment + inTextStorage:(NSTextStorage *)textStorage + withItalicFont:(UIFont *)italicFont; + +@end + +@implementation ItalicUtils + ++ (void)applyItalicForFont:(UIFont *)font + inTextStorage:(NSTextStorage *)textStorage + inRange:(NSRange)range { + UIFont *italicFont = [font setItalic]; + BOOL hasItalicFace = [italicFont isItalic]; + + NSMutableArray *clusterRanges = [NSMutableArray array]; + NSMutableArray *clusterKinds = [NSMutableArray array]; + + // we process each composed character sequence and classify it to a specific + // ItalicKind + [textStorage.string + enumerateSubstringsInRange:range + options:NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString *_Nullable cluster, + NSRange clusterRange, NSRange _, + BOOL *_Nonnull stop) { + if (cluster.length == 0) { + return; + } + [clusterRanges + addObject:[NSValue valueWithRange:clusterRange]]; + [clusterKinds + addObject:@([self kindForCluster:cluster + font:font + italicFont:italicFont + hasItalicFace:hasItalicFace])]; + }]; + + // merge neighbouring clusters of the same kind and apply the style + NSUInteger index = 0; + while (index < clusterKinds.count) { + NSUInteger endIndex = index + 1; + ItalicKind kind = (ItalicKind)[clusterKinds[index] integerValue]; + while (endIndex < clusterKinds.count && + (ItalicKind)[clusterKinds[endIndex] integerValue] == kind) { + endIndex += 1; + } + + NSRange startRange = [clusterRanges[index] rangeValue]; + NSRange endRange = [clusterRanges[endIndex - 1] rangeValue]; + NSRange segment = NSMakeRange(startRange.location, + NSMaxRange(endRange) - startRange.location); + + [self applyKind:kind + toSegment:segment + inTextStorage:textStorage + withItalicFont:italicFont]; + + index = endIndex; + } +} + +// contains all characters except whitespaces, newlines, +// control characters and ZWS ++ (NSCharacterSet *)nonNeutralCharacters { + static NSCharacterSet *nonNeutral = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableCharacterSet *set = + [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; + [set formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]]; + [set addCharactersInString:[NSString + stringWithFormat:@"%C", (unichar)0x200B]]; + nonNeutral = [[set invertedSet] copy]; + }); + return nonNeutral; +} + ++ (ItalicKind)kindForCluster:(NSString *)cluster + font:(UIFont *)font + italicFont:(UIFont *)italicFont + hasItalicFace:(BOOL)hasItalicFace { + if ([cluster rangeOfCharacterFromSet:[self nonNeutralCharacters]].location == + NSNotFound) { + return ItalicKindNone; + } + + // we just need to analyze the first unicode character to classify the whole + // cluster + unichar chars[2] = {0, 0}; + CFIndex count = 1; + chars[0] = [cluster characterAtIndex:0]; + if (CFStringIsSurrogateHighCharacter(chars[0]) && cluster.length > 1) { + chars[1] = [cluster characterAtIndex:1]; + count = 2; + } + + if (chars[0] == (unichar)NSAttachmentCharacter) { + return ItalicKindNone; + } + + BOOL coveredByFont = [font coversCharacters:chars count:count]; + + // italic style is supported - we use it + if (coveredByFont && hasItalicFace && italicFont != nullptr && + [italicFont coversCharacters:chars count:count]) { + return ItalicKindFont; + } + + // italic is not supported, we use the slant instead + return ItalicKindOblique; +} + ++ (void)applyKind:(ItalicKind)kind + toSegment:(NSRange)segment + inTextStorage:(NSTextStorage *)textStorage + withItalicFont:(UIFont *)italicFont { + switch (kind) { + case ItalicKindFont: + [textStorage addAttribute:NSFontAttributeName + value:italicFont + range:segment]; + [textStorage removeAttribute:NSObliquenessAttributeName range:segment]; + break; + case ItalicKindOblique: + [textStorage addAttribute:NSObliquenessAttributeName + value:@(kObliquenessFallback) + range:segment]; + break; + case ItalicKindNone: + [textStorage removeAttribute:NSObliquenessAttributeName range:segment]; + break; + } +} + +@end