Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .maestro/enrichedInput/screenshots/ios/inline_styles_merge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .maestro/enrichedInput/screenshots/ios/inline_styles_removal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 8 additions & 10 deletions ios/inputAttributesManager/InputAttributesManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import "ParagraphAttributesUtils.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
#import "StyleUtils.h"
#import "ZeroWidthSpaceUtils.h"

@implementation InputAttributesManager {
Expand Down Expand Up @@ -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
Comment thread
kacperzolkiewski marked this conversation as resolved.
// 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.
Expand Down
1 change: 1 addition & 0 deletions ios/interfaces/StyleBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- (BOOL)isParagraph;
- (BOOL)needsZWS;
- (BOOL)appliesStylingToTyping;
- (NSInteger)stylePriority;
- (instancetype)initWithHost:(id<EnrichedViewHost>)host;
- (NSRange)actualUsedRange:(NSRange)range;
- (void)toggle:(NSRange)range;
Expand Down
5 changes: 5 additions & 0 deletions ios/interfaces/StyleBase.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnrichedViewHost>)host {
self = [super init];
_host = host;
Expand Down
196 changes: 181 additions & 15 deletions ios/styles/ItalicStyle.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
#import "EnrichedTextInputView.h"
#import "FontExtension.h"
#import "StyleHeaders.h"
#import <CoreText/CoreText.h>

// 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

Expand All @@ -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<NSValue *> *clusterRanges = [NSMutableArray array];
NSMutableArray<NSNumber *> *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
9 changes: 8 additions & 1 deletion ios/textHtmlParser/TextHtmlParser.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
3 changes: 3 additions & 0 deletions ios/utils/StyleUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@
+ (void)removeStyleConflict:(StyleType)conflicting
from:(StyleType)conflicted
forHost:(id<EnrichedViewHost>)host;

+ (NSArray *)sortedArray:(NSArray *)array
bySortKey:(NSInteger (^)(id item))sortKeyForItem;
@end
14 changes: 14 additions & 0 deletions ios/utils/StyleUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading