Improve invisibles drawing

This commit is contained in:
1024jp 2016-03-21 18:07:02 +09:00
parent 105d7aa50b
commit 55f7e5ff0f
8 changed files with 145 additions and 144 deletions

View File

@ -25,6 +25,9 @@ develop
- Add “Copy as Rich Text” action to the contextual menu.
- Improve recovering status of unsaved documents on window resume.
- Improve line number view drawing with selection on vertical text mode.
- Improve invisibles drawing:
- Optimize drawing performance (ca. 2x).
- better drawing if anti-ailiasing is off.
- Display print progress panel as a document-modal sheet.

View File

@ -95,24 +95,20 @@
BOOL showsVerticalTab = [defaults boolForKey:CEDefaultShowOtherInvisibleCharsKey];
BOOL showsOtherInvisibles = [defaults boolForKey:CEDefaultShowOtherInvisibleCharsKey];
unichar spaceChar = [CEInvisibles spaceCharWithIndex:[defaults integerForKey:CEDefaultInvisibleSpaceKey]];
NSAttributedString *space = [[NSAttributedString alloc] initWithString:[NSString stringWithCharacters:&spaceChar length:1]
NSAttributedString *space = [[NSAttributedString alloc] initWithString:[CEInvisibles stringWithType:CEInvisibleSpace
Index:[defaults integerForKey:CEDefaultInvisibleSpaceKey]]
attributes:attributes];
unichar tabChar = [CEInvisibles tabCharWithIndex:[defaults integerForKey:CEDefaultInvisibleTabKey]];
NSAttributedString *tab = [[NSAttributedString alloc] initWithString:[NSString stringWithCharacters:&tabChar length:1]
NSAttributedString *tab = [[NSAttributedString alloc] initWithString:[CEInvisibles stringWithType:CEInvisibleTab
Index:[defaults integerForKey:CEDefaultInvisibleTabKey]]
attributes:attributes];
unichar newLineChar = [CEInvisibles newLineCharWithIndex:[defaults integerForKey:CEDefaultInvisibleNewLineKey]];
NSAttributedString *newLine = [[NSAttributedString alloc] initWithString:[NSString stringWithCharacters:&newLineChar length:1]
NSAttributedString *newLine = [[NSAttributedString alloc] initWithString:[CEInvisibles stringWithType:CEInvisibleNewLine
Index:[defaults integerForKey:CEDefaultInvisibleNewLineKey]]
attributes:attributes];
unichar fullwidthSpaceChar = [CEInvisibles fullwidthSpaceCharWithIndex:[defaults integerForKey:CEDefaultInvisibleFullwidthSpaceKey]];
NSAttributedString *fullwidthSpace = [[NSAttributedString alloc] initWithString:[NSString stringWithCharacters:&fullwidthSpaceChar length:1]
NSAttributedString *fullwidthSpace = [[NSAttributedString alloc] initWithString:[CEInvisibles stringWithType:CEInvisibleFullWidthSpace
Index:[defaults integerForKey:CEDefaultInvisibleNewLineKey]]
attributes:fullwidthAttributes];
unichar verticalTabChar = [CEInvisibles verticalTabChar];
NSAttributedString *verticalTab = [[NSAttributedString alloc] initWithString:[NSString stringWithCharacters:&verticalTabChar length:1]
NSAttributedString *verticalTab = [[NSAttributedString alloc] initWithString:[CEInvisibles stringWithType:CEInvisibleVerticalTab
Index:NULL]
attributes:attributes];
for (NSUInteger glyphIndex = glyphsToShow.location; glyphIndex < lengthToRedraw; glyphIndex++) {

View File

@ -28,8 +28,20 @@
@import Foundation;
typedef NS_ENUM(NSUInteger, CEInvisibleType) {
CEInvisibleSpace,
CEInvisibleTab,
CEInvisibleNewLine,
CEInvisibleFullWidthSpace,
CEInvisibleVerticalTab,
CEInvisibleReplacement,
};
@interface CEInvisibles : NSObject
+ (nonnull NSString *)stringWithType:(CEInvisibleType)type Index:(NSUInteger)index;
// return substitution character for invisible character
+ (unichar)spaceCharWithIndex:(NSUInteger)index;
+ (unichar)tabCharWithIndex:(NSUInteger)index;

View File

@ -49,6 +49,37 @@ static unichar const kReplacementChar = 0xFFFD; // symbol for "Other Invisibles
#pragma mark Public Methods
// ------------------------------------------------------
/// returns substitute character as NSString
+ (nonnull NSString *)stringWithType:(CEInvisibleType)type Index:(NSUInteger)index
// ------------------------------------------------------
{
unichar character;
switch (type) {
case CEInvisibleSpace:
character = [self spaceCharWithIndex:index];
break;
case CEInvisibleTab:
character = [self tabCharWithIndex:index];
break;
case CEInvisibleNewLine:
character = [self newLineCharWithIndex:index];
break;
case CEInvisibleFullWidthSpace:
character = [self fullwidthSpaceCharWithIndex:index];
break;
case CEInvisibleVerticalTab:
character = [self verticalTabChar];
break;
case CEInvisibleReplacement:
character = [self replacementChar];
break;
}
return [NSString stringWithCharacters:&character length:1];
}
// ------------------------------------------------------
/// returns substitute character for invisible space
+ (unichar)spaceCharWithIndex:(NSUInteger)index

View File

@ -35,6 +35,7 @@
@property (nonatomic) BOOL fixesLineHeight;
@property (nonatomic) BOOL usesAntialias;
@property (nonatomic, nullable) NSFont *textFont;
@property (nonatomic, nonnull) NSColor *invisiblesColor;
@property (readonly, nonatomic) CGFloat defaultLineHeightForTextFont; // defaultLineHeight for textFont
@property (readonly, nonatomic) BOOL showsOtherInvisibles;

View File

@ -40,6 +40,14 @@ static NSString * _Nonnull const HiraginoSans = @"HiraginoSans-W3"; // since OS
static NSString * _Nonnull const HiraKakuProN = @"HiraKakuProN-W3";
// convenient function
CTLineRef createCTLineRefWithString(NSString *string, NSDictionary *attributes)
{
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
return CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString);
}
@interface CELayoutManager ()
@property (nonatomic) BOOL showsSpace;
@ -47,10 +55,8 @@ static NSString * _Nonnull const HiraKakuProN = @"HiraKakuProN-W3";
@property (nonatomic) BOOL showsNewLine;
@property (nonatomic) BOOL showsFullwidthSpace;
@property (nonatomic) unichar spaceChar;
@property (nonatomic) unichar tabChar;
@property (nonatomic) unichar newLineChar;
@property (nonatomic) unichar fullwidthSpaceChar;
@property (nonatomic, nonnull, copy) NSArray<NSString *> *invisibles;
@property (nonatomic, nullable) NSArray<id> *invisibleLines; // array of CTLineRef
@property (nonatomic) CGFloat spaceWidth;
@ -100,12 +106,17 @@ static NSString *HiraginoSansName;
self = [super init];
if (self) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
_spaceChar = [CEInvisibles spaceCharWithIndex:[defaults integerForKey:CEDefaultInvisibleSpaceKey]];
_tabChar = [CEInvisibles tabCharWithIndex:[defaults integerForKey:CEDefaultInvisibleTabKey]];
_newLineChar = [CEInvisibles newLineCharWithIndex:[defaults integerForKey:CEDefaultInvisibleNewLineKey]];
_fullwidthSpaceChar = [CEInvisibles fullwidthSpaceCharWithIndex:[defaults integerForKey:CEDefaultInvisibleFullwidthSpaceKey]];
_invisiblesColor = [NSColor disabledControlTextColor];
_invisibles = @[[CEInvisibles stringWithType:CEInvisibleSpace Index:[defaults integerForKey:CEDefaultInvisibleSpaceKey]],
[CEInvisibles stringWithType:CEInvisibleTab Index:[defaults integerForKey:CEDefaultInvisibleTabKey]],
[CEInvisibles stringWithType:CEInvisibleNewLine Index:[defaults integerForKey:CEDefaultInvisibleNewLineKey]],
[CEInvisibles stringWithType:CEInvisibleFullWidthSpace Index:[defaults integerForKey:CEDefaultInvisibleFullwidthSpaceKey]],
[CEInvisibles stringWithType:CEInvisibleVerticalTab Index:NULL],
[CEInvisibles stringWithType:CEInvisibleReplacement Index:NULL],
];
// setShowsInvisibles: CEEditorViewController CEPrintView
_showsSpace = [defaults boolForKey:CEDefaultShowInvisibleSpaceKey];
_showsTab = [defaults boolForKey:CEDefaultShowInvisibleTabKey];
@ -158,7 +169,7 @@ static NSString *HiraginoSansName;
// ------------------------------------------------------
///
/// draw invisible characters
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin
// ------------------------------------------------------
{
@ -168,106 +179,72 @@ static NSString *HiraginoSansName;
}
// draw invisibles
if ([self showsInvisibles]) {
if ([self showsInvisibles] || [[self invisibleLines] count] > 0) {
CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
NSString *completeString = [[self textStorage] string];
NSUInteger lengthToRedraw = NSMaxRange(glyphsToShow);
//
CTFontRef font = (__bridge CTFontRef)[self textFont];
NSColor *color = [[self theme] invisiblesColor];
CGFloat baselineOffset = [self defaultBaselineOffsetForFont:[self textFont]];
// set graphics context
CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, [color CGColor]);
CGMutablePathRef paths = CGPathCreateMutable();
// adjust drawing coordinate
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1.0, -1.0); // flip
transform = CGAffineTransformTranslate(transform, origin.x, - origin.y);
CGContextConcatCTM(context, transform);
// prepare glyphs
CGPathRef spaceGlyphPath = glyphPathWithCharacter([self spaceChar], font, false);
CGPathRef tabGlyphPath = glyphPathWithCharacter([self tabChar], font, false);
CGPathRef newLineGlyphPath = glyphPathWithCharacter([self newLineChar], font, false);
CGPathRef fullWidthSpaceGlyphPath = glyphPathWithCharacter([self fullwidthSpaceChar], font, true);
CGPathRef verticalTabGlyphPath = glyphPathWithCharacter([CEInvisibles verticalTabChar], font, true);
CGPathRef replacementGlyphPath = glyphPathWithCharacter([CEInvisibles replacementChar], font, true);
// store value to avoid accessing properties each time (2014-07 by 1024jp)
BOOL showsSpace = [self showsSpace];
BOOL showsTab = [self showsTab];
BOOL showsNewLine = [self showsNewLine];
BOOL showsFullwidthSpace = [self showsFullwidthSpace];
BOOL showsVerticalTab = [self showsOtherInvisibles]; // Vertical tab belongs to other invisibles.
BOOL showsOtherInvisibles = [self showsOtherInvisibles];
// flip coordinate if needed
if ([[NSGraphicsContext currentContext] isFlipped]) {
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0, -1.0));
}
// draw invisibles glyph by glyph
for (NSUInteger glyphIndex = glyphsToShow.location; glyphIndex < lengthToRedraw; glyphIndex++) {
for (NSUInteger glyphIndex = glyphsToShow.location; glyphIndex < NSMaxRange(glyphsToShow); glyphIndex++) {
NSUInteger charIndex = [self characterIndexForGlyphAtIndex:glyphIndex];
unichar character = [completeString characterAtIndex:charIndex];
CGPathRef glyphPath;
CEInvisibleType invisibleType;
switch (character) {
case ' ':
case 0x00A0:
if (!showsSpace) { continue; }
glyphPath = spaceGlyphPath;
if (![self showsSpace]) { continue; }
invisibleType = CEInvisibleSpace;
break;
case '\t':
if (!showsTab) { continue; }
glyphPath = tabGlyphPath;
if (![self showsTab]) { continue; }
invisibleType = CEInvisibleTab;
break;
case '\n':
if (!showsNewLine) { continue; }
glyphPath = newLineGlyphPath;
if (![self showsNewLine]) { continue; }
invisibleType = CEInvisibleNewLine;
break;
case 0x3000: // fullwidth-space (JP)
if (!showsFullwidthSpace) { continue; }
glyphPath = fullWidthSpaceGlyphPath;
if (![self showsFullwidthSpace]) { continue; }
invisibleType = CEInvisibleFullWidthSpace;
break;
case '\v':
if (!showsVerticalTab) { continue; }
glyphPath = verticalTabGlyphPath;
if (![self showsOtherInvisibles]) { continue; } // Vertical tab belongs to other invisibles.
invisibleType = CEInvisibleVerticalTab;
break;
default:
if (!showsOtherInvisibles || ([self glyphAtIndex:glyphIndex isValidIndex:NULL] != NSControlGlyph)) { continue; }
if (![self showsOtherInvisibles] || ([self glyphAtIndex:glyphIndex isValidIndex:NULL] != NSControlGlyph)) { continue; }
// Skip the second glyph if character is a surrogate-pair
if (CFStringIsSurrogateLowCharacter(character) &&
((charIndex > 0) && CFStringIsSurrogateHighCharacter([completeString characterAtIndex:charIndex - 1])))
{
continue;
}
glyphPath = replacementGlyphPath;
invisibleType = CEInvisibleReplacement;
}
// add invisible char path
NSPoint point = [self pointToDrawGlyphAtIndex:glyphIndex verticalOffset:baselineOffset];
CGAffineTransform translate = CGAffineTransformMakeTranslation(point.x, -point.y);
CGPathAddPath(paths, &translate, glyphPath);
CTLineRef line = (__bridge CTLineRef)[self invisibleLines][invisibleType];
// calcurate position to draw glyph
NSPoint point = [self lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:NULL withoutAdditionalLayout:YES].origin;
NSPoint glyphLocation = [self locationForGlyphAtIndex:glyphIndex];
point.x += glyphLocation.x + origin.x;
point.y += baselineOffset + origin.y;
// draw character
CGContextSetTextPosition(context, point.x, point.y);
CTLineDraw(line, context);
}
// draw invisible glyphs (excl. other invisibles)
CGContextAddPath(context, paths);
CGContextFillPath(context);
// release
CGContextRestoreGState(context);
CGPathRelease(paths);
CGPathRelease(spaceGlyphPath);
CGPathRelease(tabGlyphPath);
CGPathRelease(newLineGlyphPath);
CGPathRelease(fullWidthSpaceGlyphPath);
CGPathRelease(verticalTabGlyphPath);
CGPathRelease(replacementGlyphPath);
}
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
@ -298,7 +275,7 @@ static NSString *HiraginoSansName;
#pragma mark Public Methods
// ------------------------------------------------------
///
/// set text font to use and cache values
- (void)setTextFont:(nullable NSFont *)textFont
// ------------------------------------------------------
{
@ -315,6 +292,19 @@ static NSString *HiraginoSansName;
// store width of space char for hanging indent width calculation
NSFont *screenFont = [textFont screenFont] ? : textFont;
[self setSpaceWidth:[screenFont advancementForGlyph:(NSGlyph)' '].width];
[self invalidateInvisiblesStyle];
}
// ------------------------------------------------------
/// update invisibles color
- (void)setInvisiblesColor:(NSColor *)invisiblesColor
// ------------------------------------------------------
{
_invisiblesColor = invisiblesColor;
[self invalidateInvisiblesStyle];
}
@ -436,64 +426,29 @@ static NSString *HiraginoSansName;
#pragma mark Private Methods
// ------------------------------------------------------
/// current theme
- (nullable CETheme *)theme
/// cache CTLineRefs for invisible characters drawing
- (void)invalidateInvisiblesStyle
// ------------------------------------------------------
{
return [(NSTextView<CETextViewProtocol> *)[self firstTextView] theme];
}
//------------------------------------------------------
///
- (NSPoint)pointToDrawGlyphAtIndex:(NSUInteger)glyphIndex verticalOffset:(CGFloat)offset
//------------------------------------------------------
{
NSPoint origin = [self lineFragmentRectForGlyphAtIndex:glyphIndex
effectiveRange:NULL
withoutAdditionalLayout:YES].origin;
NSPoint glyphLocation = [self locationForGlyphAtIndex:glyphIndex];
origin.x += glyphLocation.x;
origin.y += offset;
return origin;
}
//------------------------------------------------------
///
CGPathRef glyphPathWithCharacter(unichar character, CTFontRef font, bool prefersFullWidth)
//------------------------------------------------------
{
CGFloat fontSize = CTFontGetSize(font);
CGGlyph glyph;
NSFont *font;
if (usesTextFontForInvisibles) {
if (CTFontGetGlyphsForCharacters(font, &character, &glyph, 1)) {
return CTFontCreatePathForGlyph(font, glyph, NULL);
}
font = [self textFont];
} else {
CGFloat fontSize = [[self textFont] pointSize];
font = [[NSFont fontWithName:@"LucidaGrande" size:fontSize] screenFont] ?: [NSFont systemFontOfSize:fontSize];
}
NSDictionary<NSString *, id> *attributes = @{NSForegroundColorAttributeName: [self invisiblesColor],
NSFontAttributeName: font};
NSDictionary<NSString *, id> *fullWidthAttributes = @{NSForegroundColorAttributeName: [self invisiblesColor],
NSFontAttributeName: [[NSFont fontWithName:HiraginoSansName size:[font pointSize]] screenFont] ?: font};
// try fallback fonts in cases where user font doesn't support the input charactor
// - All invisible characters of choices can be covered with the following two fonts.
// - Monaco for vertical tab
CGPathRef path = NULL;
NSArray<NSString *> *fallbackFontNames = (prefersFullWidth
? @[HiraginoSansName, @"LucidaGrande", @"Monaco"]
: @[@"LucidaGrande", HiraginoSansName, @"Monaco"]);
for (NSString *fontName in fallbackFontNames) {
CTFontRef fallbackFont = CTFontCreateWithName((CFStringRef)fontName, fontSize, 0);
if (CTFontGetGlyphsForCharacters(fallbackFont, &character, &glyph, 1)) {
path = CTFontCreatePathForGlyph(fallbackFont, glyph, NULL);
CFRelease(fallbackFont);
break;
}
CFRelease(fallbackFont);
}
return path;
[self setInvisibleLines:@[(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleSpace], attributes),
(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleTab], attributes),
(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleNewLine], attributes),
(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleFullWidthSpace], fullWidthAttributes),
(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleVerticalTab], fullWidthAttributes),
(__bridge_transfer id)createCTLineRefWithString([self invisibles][CEInvisibleReplacement], fullWidthAttributes),
]];
}
@end

View File

@ -391,6 +391,7 @@ static NSString *_Nonnull const PageNumberPlaceholder = @"PAGENUM";
[self setTheme:[[CEThemeManager sharedManager] themeWithName:settings[CEPrintThemeKey]]];
[self setTextColor:[[self theme] textColor]];
[self setBackgroundColor:[[self theme] backgroundColor]];
[(CELayoutManager *)[self layoutManager] setInvisiblesColor:[[self theme] invisiblesColor]];
// perform coloring
if (![self syntaxStyle]) {

View File

@ -1159,6 +1159,8 @@ static NSCharacterSet *kMatchingClosingBracketsSet;
[self setInsertionPointColor:[theme insertionPointColor]];
[self setSelectedTextAttributes:@{NSBackgroundColorAttributeName: [theme selectionColor]}];
[(CELayoutManager *)[self layoutManager] setInvisiblesColor:[theme invisiblesColor]];
//
NSInteger knobStyle = [theme isDarkTheme] ? NSScrollerKnobStyleLight : NSScrollerKnobStyleDefault;
[[self enclosingScrollView] setScrollerKnobStyle:knobStyle];