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/inputAttributesManager/InputAttributesManager.mm b/ios/inputAttributesManager/InputAttributesManager.mm index fc07d871f..9445b8f52 100644 --- a/ios/inputAttributesManager/InputAttributesManager.mm +++ b/ios/inputAttributesManager/InputAttributesManager.mm @@ -5,6 +5,7 @@ #import "ParagraphAttributesUtils.h" #import "RangeUtils.h" #import "StyleHeaders.h" +#import "StyleUtils.h" #import "ZeroWidthSpaceUtils.h" @implementation InputAttributesManager { @@ -97,16 +98,13 @@ - (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. - 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; - }]; + // styles override them on their specific sub-ranges. Inline styles among + // themselves follow their stylePriority. + NSArray *sortedStyleTypes = + [StyleUtils sortedArray:presentStyles.allKeys + bySortKey:^NSInteger(NSNumber *styleType) { + return [_input->stylesDict[styleType] stylePriority]; + }]; // re-apply meta-attributes and apply visual styling following the saved // occurences. 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..7f56478fb 100644 --- a/ios/styles/ItalicStyle.mm +++ b/ios/styles/ItalicStyle.mm @@ -1,6 +1,46 @@ #import "EnrichedTextInputView.h" #import "FontExtension.h" #import "StyleHeaders.h" +#import + +// slant used when a font has no italic face +static const CGFloat kObliquenessFallback = 0.2; + +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, +}; + +static NSCharacterSet *NonNeutralCharacters(void) { + static NSCharacterSet *nonNeutral = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableCharacterSet *set = + [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; + [set formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]]; + // ZWS + [set addCharactersInString:[NSString + stringWithFormat:@"%C", (unichar)0x200B]]; + nonNeutral = [[set invertedSet] copy]; + }); + return nonNeutral; +} + +// returns YES when the font renders the given UTF-16 sequence itself +static BOOL FontCoversCharacters(UIFont *font, const unichar *chars, + CFIndex count) { + if (font == nullptr) { + return NO; + } + CGGlyph glyphs[2] = {0, 0}; + return CTFontGetGlyphsForCharacters((__bridge CTFontRef)font, chars, glyphs, + count); +} @implementation ItalicStyle : StyleBase @@ -17,21 +57,147 @@ - (BOOL)isParagraph { } - (void)applyStyling:(NSRange)range { - [self.host.textView.textStorage - enumerateAttribute:NSFontAttributeName - inRange:range - options:0 - usingBlock:^(id _Nullable value, NSRange range, - BOOL *_Nonnull stop) { - UIFont *font = (UIFont *)value; - if (font != nullptr) { - UIFont *newFont = [font setItalic]; - [self.host.textView.textStorage - addAttribute:NSFontAttributeName - value:newFont - range:range]; - } - }]; + [self applyItalicInTextStorage:self.host.textView.textStorage inRange:range]; +} + +// some styles might apply a new font (inline code), so we need to apply +// the italic last, that way knowing if the used font supports italics +// or we need to apply a slant +- (NSInteger)stylePriority { + return 3; +} + +- (void)applyItalicInTextStorage:(NSTextStorage *)textStorage + inRange:(NSRange)range { + if (textStorage == nullptr || range.length == 0 || + NSMaxRange(range) > textStorage.length) { + return; + } + + // we process each present font + [textStorage enumerateAttribute:NSFontAttributeName + inRange:range + options:0 + usingBlock:^(id _Nullable value, NSRange fontRange, + BOOL *_Nonnull stop) { + UIFont *font = (UIFont *)value; + if (font == nullptr) { + return; + } + [self applyItalicForFont:font + inTextStorage:textStorage + inRange:fontRange]; + }]; +} + +- (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; + } +} + +- (ItalicKind)kindForCluster:(NSString *)cluster + font:(UIFont *)font + italicFont:(UIFont *)italicFont + hasItalicFace:(BOOL)hasItalicFace { + if ([cluster rangeOfCharacterFromSet: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 = FontCoversCharacters(font, chars, count); + + // italic style is supported - we use it + if (coveredByFont && hasItalicFace && + FontCoversCharacters(italicFont, chars, 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 diff --git a/ios/textHtmlParser/TextHtmlParser.mm b/ios/textHtmlParser/TextHtmlParser.mm index 3899fd2d8..59bc93896 100644 --- a/ios/textHtmlParser/TextHtmlParser.mm +++ b/ios/textHtmlParser/TextHtmlParser.mm @@ -153,8 +153,15 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles { } } + // Respect the styling priority + NSArray *sortedInlineApply = + [StyleUtils sortedArray:pendingInlineApply + bySortKey:^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/StyleUtils.h b/ios/utils/StyleUtils.h index ae3763cb4..70dab8b05 100644 --- a/ios/utils/StyleUtils.h +++ b/ios/utils/StyleUtils.h @@ -30,4 +30,7 @@ + (void)removeStyleConflict:(StyleType)conflicting from:(StyleType)conflicted forHost:(id)host; + ++ (NSArray *)sortedArray:(NSArray *)array + bySortKey:(NSInteger (^)(id item))sortKeyForItem; @end diff --git a/ios/utils/StyleUtils.mm b/ios/utils/StyleUtils.mm index 838269221..21d3d7a9b 100644 --- a/ios/utils/StyleUtils.mm +++ b/ios/utils/StyleUtils.mm @@ -287,4 +287,18 @@ + (void)removeStyleConflict:(StyleType)conflicting } } ++ (NSArray *)sortedArray:(NSArray *)array + bySortKey:(NSInteger (^)(id item))sortKeyForItem { + return [array 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