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..17e011754 100644 --- a/ios/inputAttributesManager/InputAttributesManager.mm +++ b/ios/inputAttributesManager/InputAttributesManager.mm @@ -61,6 +61,14 @@ - (void)clearRemovedTypingAttributes { [_removedTypingAttributes removeAllObjects]; } +// Paragraph styles always go first, inline ones are ordered by their +// stylePriority +- (NSInteger)stylingOrderFor:(StyleBase *)style { + if (style == nullptr) + return NSIntegerMax; + return [style isParagraph] ? NSIntegerMin : [style stylePriority]; +} + - (void)handleDirtyRangesStyling { // Filter out 0 length ranges for styling. NSPredicate *predicate = [NSPredicate @@ -97,16 +105,21 @@ - (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; - }]; + sortedArrayWithOptions:NSSortStable + usingComparator:^NSComparisonResult(NSNumber *a, NSNumber *b) { + NSInteger aOrder = + [self stylingOrderFor:_input->stylesDict[a]]; + NSInteger bOrder = + [self stylingOrderFor:_input->stylesDict[b]]; + if (aOrder == bOrder) { + return [a compare:b]; + } + return aOrder < bOrder ? NSOrderedAscending + : NSOrderedDescending; + }]; // 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..c6e14b088 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 0; +} + - (instancetype)initWithHost:(id)host { self = [super init]; _host = host; diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm index f2a161141..39b965eb4 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 @@ -17,21 +17,15 @@ - (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]; - } - }]; + [ItalicUtils 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; } @end diff --git a/ios/textHtmlParser/TextHtmlParser.mm b/ios/textHtmlParser/TextHtmlParser.mm index 3899fd2d8..df5a8e472 100644 --- a/ios/textHtmlParser/TextHtmlParser.mm +++ b/ios/textHtmlParser/TextHtmlParser.mm @@ -153,8 +153,20 @@ - (void)applyProcessedStyles:(NSArray *_Nonnull)processedStyles { } } + // Respect the styling priority + NSArray *sortedInlineApply = [pendingInlineApply + sortedArrayWithOptions:NSSortStable + usingComparator:^NSComparisonResult(NSArray *a, NSArray *b) { + NSInteger aPriority = [((StyleBase *)a[0]) stylePriority]; + NSInteger bPriority = [((StyleBase *)b[0]) stylePriority]; + if (aPriority == bPriority) + return NSOrderedSame; + return aPriority < bPriority ? NSOrderedAscending + : NSOrderedDescending; + }]; + // 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..252e1b2d5 --- /dev/null +++ b/ios/utils/ItalicUtils.h @@ -0,0 +1,8 @@ +#import +#pragma once + +@interface ItalicUtils : NSObject + ++ (void)applyItalicInTextStorage:(NSTextStorage *)textStorage + inRange:(NSRange)range; +@end diff --git a/ios/utils/ItalicUtils.mm b/ios/utils/ItalicUtils.mm new file mode 100644 index 000000000..c518674f0 --- /dev/null +++ b/ios/utils/ItalicUtils.mm @@ -0,0 +1,179 @@ +#import "ItalicUtils.h" +#import "FontExtension.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 ItalicUtils + ++ (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