Skip to content

Commit 36c4629

Browse files
committed
Fix CoreText clipping issues with tall texts
This fixes the issue where particularly tall characters will get clipped at the row boundary. This happens because even though a font describes the line height with font metrics, individual glyphs do not have to respect them, and we can see with emoji rendering sometimes they can poke upwards past the line height. Also, it's trivially easy to construct composing characters that become really tall, e.g. "x゙̂⃗", or Tibetan scripts like "ཧཱུྃ". To fix this, we do the following: 1. Remove the explicit clipping call at rendering. 2. Fix partial redraw to not lead to clipping / corruption. This is quite tricky, because let's say we have a character that is tall enough to touch other lines, if those lines are redraw but not the line with the tall char, the redraw will paint over the parts of the glyphs poking through. Alternatively if we redraw the line with the tall chars we also need to expand the redraw region to make sure other lines get repainted as well. To fix this properly, we should do a proper glyph calculation when we receive the draw command before we issue before we call `setNeedsDisplayInRect`, but since right now we only extract glyph info later (during drawRect call), it's too late. We just do the hacky solution by storing a variable `redrawExpandRows` that tracks how many lines to expand for all lines. It's a little hacky since this will affect all lines permanently regardless if they have tall characters or not. The proper fix may come later as an optimization (or when we do hardware rendering via Metal). 3. Re-order the rendering so we have a two pass system, where we first draw the background fill color for all rows, then the text. This helps prevent things like Vim's window split or cursorline from obscuring the text. 4. Add a preference to turn on clipping (old behavior). This helps prevent odd issues with really tall texts (e.g. Zalgo text) making it hard to see what's going on. The preference `MMRendererClipToRow` is not exposed in UI for now as it's a relatively niche. It will be exposed later when we have a dedicated render tab in settings. Note that a lot of these characters only show their full height by doing `set maxcombine=8` because the default (2) is quite low. Part of the fix for #995
1 parent 202e79b commit 36c4629

File tree

7 files changed

+171
-65
lines changed

7 files changed

+171
-65
lines changed

runtime/doc/gui_mac.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ KEY VALUE ~
290290
*MMNoTitleBarWindow* hide title bar [bool]
291291
*MMTitlebarAppearsTransparent* enable a transparent titlebar [bool]
292292
*MMAppearanceModeSelection* dark mode selection (|macvim-dark-mode|)[bool]
293+
*MMRendererClipToRow* clip tall characters to the row they are on [bool]
293294
*MMSmoothResize* allow smooth resizing of MacVim window [bool]
294295
*MMShareFindPboard* share search text to Find Pasteboard [bool]
295296
*MMShowAddTabButton* enable "add tab" button on tabline [bool]

runtime/doc/tags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5498,6 +5498,7 @@ MMNoFontSubstitution gui_mac.txt /*MMNoFontSubstitution*
54985498
MMNoTitleBarWindow gui_mac.txt /*MMNoTitleBarWindow*
54995499
MMNonNativeFullScreenSafeAreaBehavior gui_mac.txt /*MMNonNativeFullScreenSafeAreaBehavior*
55005500
MMNonNativeFullScreenShowMenu gui_mac.txt /*MMNonNativeFullScreenShowMenu*
5501+
MMRendererClipToRow gui_mac.txt /*MMRendererClipToRow*
55015502
MMShareFindPboard gui_mac.txt /*MMShareFindPboard*
55025503
MMShowAddTabButton gui_mac.txt /*MMShowAddTabButton*
55035504
MMSmoothResize gui_mac.txt /*MMSmoothResize*

src/MacVim/MMAppController.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ + (void)initialize
260260
[NSNumber numberWithBool:YES], MMShareFindPboardKey,
261261
[NSNumber numberWithBool:NO], MMSmoothResizeKey,
262262
[NSNumber numberWithBool:NO], MMCmdLineAlignBottomKey,
263+
[NSNumber numberWithBool:NO], MMRendererClipToRowKey,
263264
[NSNumber numberWithBool:YES], MMAllowForceClickLookUpKey,
264265
[NSNumber numberWithBool:NO], MMUpdaterPrereleaseChannelKey,
265266
nil];

src/MacVim/MMCoreTextView.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
@class MMTextViewHelper;
1414

15+
NS_ASSUME_NONNULL_BEGIN
16+
1517

1618
/// The main text view that manages drawing Vim's content using Core Text, and
1719
/// handles input. We are using this instead of NSTextView because of the
@@ -84,7 +86,7 @@
8486
//
8587
// NSFontChanging methods
8688
//
87-
- (void)changeFont:(id)sender;
89+
- (void)changeFont:(nullable id)sender;
8890

8991
//
9092
// NSMenuItemValidation
@@ -100,7 +102,7 @@
100102
- (IBAction)paste:(id)sender;
101103
- (IBAction)undo:(id)sender;
102104
- (IBAction)redo:(id)sender;
103-
- (IBAction)selectAll:(id)sender;
105+
- (IBAction)selectAll:(nullable id)sender;
104106

105107
//
106108
// MMTextStorage methods
@@ -186,3 +188,5 @@
186188
@interface MMCoreTextView (ToolTip)
187189
- (void)setToolTipAtMousePoint:(NSString *)string;
188190
@end
191+
192+
NS_ASSUME_NONNULL_END

src/MacVim/MMCoreTextView.m

Lines changed: 160 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ @implementation MMCoreTextView {
225225

226226
BOOL alignCmdLineToBottom; ///< Whether to pin the Vim command-line to the bottom of the window
227227
int cmdlineRow; ///< Row number (0-indexed) where the cmdline starts. Used for pinning it to the bottom if desired.
228+
229+
/// Number of rows to expand when redrawing to make sure we don't clip tall
230+
/// characters whose glyphs extend beyond the bottom/top of the cell.
231+
///
232+
/// Note: This is a short-term hacky solution as it permanently increases
233+
/// the number of rows to expand every time we redraw. Eventually we should
234+
/// calculate each line's glyphs' bounds before issuing a redraw and use
235+
/// that to determine the accurate redraw bounds instead. Currently we
236+
/// calculate the glyph run too late (inside the draw call itself).
237+
unsigned int redrawExpandRows;
228238
}
229239

230240
- (instancetype)initWithFrame:(NSRect)frame
@@ -255,6 +265,7 @@ - (instancetype)initWithFrame:(NSRect)frame
255265

256266
alignCmdLineToBottom = NO; // this would be updated to the user preferences later
257267
cmdlineRow = -1; // this would be updated by Vim
268+
redrawExpandRows = 0; // start at 0, until we see a tall character. and then we expand it.
258269

259270
return self;
260271
}
@@ -739,11 +750,16 @@ - (BOOL)isFlipped
739750

740751
- (void)setNeedsDisplayFromRow:(int)row column:(int)col toRow:(int)row2
741752
column:(int)col2 {
753+
row -= redrawExpandRows;
754+
row2 += redrawExpandRows;
742755
[self setNeedsDisplayInRect:[self rectForRow:row column:0 numRows:row2-row+1 numColumns:maxColumns]];
743756
}
744757

745758
- (void)drawRect:(NSRect)rect
746759
{
760+
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
761+
const BOOL clipTextToRow = [ud boolForKey:MMRendererClipToRowKey]; // Specify whether to clip tall characters by the row boundary.
762+
747763
NSGraphicsContext *context = [NSGraphicsContext currentContext];
748764
#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10
749765
CGContextRef ctx = context.CGContext;
@@ -777,14 +793,101 @@ - (void)drawRect:(NSRect)rect
777793

778794
CGContextFillRect(ctx, rect);
779795

780-
for (size_t r = 0; r < grid.rows; r++) {
781-
const CGRect rowRect = [self rectForRow:r column:0 numRows:1 numColumns:grid.cols];
782-
const CGRect rowClipRect = CGRectIntersection(rowRect, rect);
783-
if (CGRectIsNull(rowClipRect))
784-
continue;
785-
CGContextSaveGState(ctx);
786-
CGContextClipToRect(ctx, rowClipRect);
787-
796+
// Function to draw all rows
797+
void (^drawAllRows)(void (^)(CGContextRef,CGRect,int)) = ^(void (^drawFunc)(CGContextRef,CGRect,int)){
798+
for (size_t r = 0; r < grid.rows; r++) {
799+
const CGRect rowRect = [self rectForRow:(int)r
800+
column:0
801+
numRows:1
802+
numColumns:grid.cols];
803+
804+
// Expand the clip rect to include some above/below rows in case we have tall characters.
805+
const CGRect rowExpandedRect = [self rectForRow:(int)(r-redrawExpandRows)
806+
column:0
807+
numRows:(1+redrawExpandRows*2)
808+
numColumns:grid.cols];
809+
810+
const CGRect rowClipRect = CGRectIntersection(rowExpandedRect, rect);
811+
if (CGRectIsNull(rowClipRect))
812+
continue;
813+
CGContextSaveGState(ctx);
814+
if (clipTextToRow)
815+
CGContextClipToRect(ctx, rowClipRect);
816+
817+
drawFunc(ctx, rowRect, (int)r);
818+
819+
CGContextRestoreGState(ctx);
820+
}
821+
};
822+
823+
// Function to draw a row of background colors, signs, and cursor rect. These should go below
824+
// any text.
825+
void (^drawBackgroundAndCursorFunc)(CGContextRef,CGRect,int) = ^(CGContextRef ctx, CGRect rowRect, int r){
826+
for (int c = 0; c < grid.cols; c++) {
827+
GridCell cell = *grid_cell(&grid, r, c);
828+
CGRect cellRect = {{rowRect.origin.x + cellSize.width * c, rowRect.origin.y}, cellSize};
829+
if (cell.textFlags & DRAW_WIDE)
830+
cellRect.size.width *= 2;
831+
if (cell.inverted) {
832+
cell.bg ^= 0xFFFFFF;
833+
cell.fg ^= 0xFFFFFF;
834+
cell.sp ^= 0xFFFFFF;
835+
}
836+
837+
// Fill background
838+
if (cell.bg != defaultBg && ALPHA(cell.bg) > 0) {
839+
CGRect fillCellRect = cellRect;
840+
841+
if (c == grid.cols - 1 || (c == grid.cols - 2 && (cell.textFlags & DRAW_WIDE))) {
842+
// Fill a little extra to the right if this is the last
843+
// column, and the frame size isn't exactly the same size
844+
// as the grid (due to smooth resizing, etc). This makes it
845+
// look less ugly and more consisten. See rectForRow:'s
846+
// implementation for extra comments.
847+
CGFloat extraWidth = rowRect.origin.x + rowRect.size.width - (cellRect.size.width + cellRect.origin.x);
848+
fillCellRect.size.width += extraWidth;
849+
}
850+
851+
CGContextSetFillColor(ctx, COMPONENTS(cell.bg));
852+
CGContextFillRect(ctx, fillCellRect);
853+
}
854+
855+
// Handle signs
856+
if (cell.sign) {
857+
CGRect signRect = cellRect;
858+
signRect.size.width *= 2;
859+
[cell.sign drawInRect:signRect
860+
fromRect:(NSRect){{0, 0}, cell.sign.size}
861+
operation:(cell.inverted ? NSCompositingOperationDifference : NSCompositingOperationSourceOver)
862+
fraction:1.0];
863+
}
864+
865+
// Insertion point (cursor)
866+
if (cell.insertionPoint.color && cell.insertionPoint.fraction) {
867+
float frac = cell.insertionPoint.fraction / 100.0;
868+
NSRect rect = cellRect;
869+
if (MMInsertionPointHorizontal == cell.insertionPoint.shape) {
870+
rect.size.height = cellSize.height * frac;
871+
} else if (MMInsertionPointVertical == cell.insertionPoint.shape) {
872+
rect.size.width = cellSize.width * frac;
873+
} else if (MMInsertionPointVerticalRight == cell.insertionPoint.shape) {
874+
rect.size.width = cellSize.width * frac;
875+
rect.origin.x += cellRect.size.width - rect.size.width;
876+
}
877+
rect = [self backingAlignedRect:rect options:NSAlignAllEdgesInward];
878+
879+
[[NSColor colorWithArgbInt:cell.insertionPoint.color] set];
880+
if (MMInsertionPointHollow == cell.insertionPoint.shape) {
881+
[NSBezierPath strokeRect:NSInsetRect(rect, 0.5, 0.5)];
882+
} else {
883+
NSRectFill(rect);
884+
}
885+
}
886+
}
887+
};
888+
889+
// Function to draw a row of text with their corresponding text styles.
890+
void (^drawTextFunc)(CGContextRef,CGRect,int) = ^(CGContextRef ctx, CGRect rowRect, int r){
788891
__block NSMutableString *lineString = nil;
789892
__block CGFloat lineStringStart = 0;
790893
__block CFRange lineStringRange = {};
@@ -797,7 +900,7 @@ - (void)drawRect:(NSRect)rect
797900
if (!lineString.length)
798901
return;
799902
size_t cellOffsetByIndex[lineString.length];
800-
for (size_t i = 0, stringIndex = 0; i < lineStringRange.length; i++) {
903+
for (int i = 0, stringIndex = 0; i < lineStringRange.length; i++) {
801904
GridCell cell = *grid_cell(&grid, r, lineStringRange.location + i);
802905
size_t cell_length = cell.string.length;
803906
for (size_t j = 0; j < cell_length; j++) {
@@ -860,6 +963,38 @@ - (void)drawRect:(NSRect)rect
860963
layoutPositions = layoutPositions_storage;
861964
}
862965

966+
const int maxRedrawExpandRows = clipTextToRow ? 0 : 3; // Hard-code a sane maximum for now to prevent degenerate edge cases
967+
if (redrawExpandRows < maxRedrawExpandRows) {
968+
// Check if we encounter any glyphs in this line that are too tall and would be
969+
// clipped / not redrawn properly. If we encounter that, increase
970+
// redrawExpandRows and redraw.
971+
// Note: This is kind of a hacky solution. See comments for redrawExpandRows.
972+
CGRect lineBounds = CTRunGetImageBounds(run, ctx, CFRangeMake(0,0));
973+
if (!CGRectIsNull(lineBounds)) {
974+
unsigned int newRedrawExpandRows = 0;
975+
if (lineBounds.origin.y < rowRect.origin.y) {
976+
newRedrawExpandRows = (int)ceil((rowRect.origin.y - lineBounds.origin.y) / cellSize.height);
977+
}
978+
if (lineBounds.origin.y + lineBounds.size.height > rowRect.origin.y + cellSize.height) {
979+
int rowsAbove = (int)ceil(((lineBounds.origin.y + lineBounds.size.height) - (rowRect.origin.y + cellSize.height)) / cellSize.height);
980+
if (rowsAbove > newRedrawExpandRows) {
981+
newRedrawExpandRows = rowsAbove;
982+
}
983+
}
984+
985+
if (newRedrawExpandRows > redrawExpandRows) {
986+
redrawExpandRows = newRedrawExpandRows;
987+
if (redrawExpandRows > maxRedrawExpandRows) {
988+
redrawExpandRows = maxRedrawExpandRows;
989+
}
990+
[self setNeedsDisplay:YES];
991+
}
992+
}
993+
}
994+
else {
995+
redrawExpandRows = maxRedrawExpandRows;
996+
}
997+
863998
for (CFIndex i = 0; i < glyphCount; i++) {
864999
if (indices[i] >= lineStringLength) {
8651000
ASLogDebug(@"Invalid glyph pos index: %ld, len: %lu", (long)indices[i], (unsigned long)lineStringLength);
@@ -916,7 +1051,7 @@ - (void)drawRect:(NSRect)rect
9161051

9171052
BOOL hasStrikeThrough = NO;
9181053

919-
for (size_t c = 0; c < grid.cols; c++) {
1054+
for (int c = 0; c < grid.cols; c++) {
9201055
GridCell cell = *grid_cell(&grid, r, c);
9211056
CGRect cellRect = {{rowRect.origin.x + cellSize.width * c, rowRect.origin.y}, cellSize};
9221057
if (cell.textFlags & DRAW_WIDE)
@@ -927,56 +1062,6 @@ - (void)drawRect:(NSRect)rect
9271062
cell.sp ^= 0xFFFFFF;
9281063
}
9291064

930-
// Fill background
931-
if (cell.bg != defaultBg && ALPHA(cell.bg) > 0) {
932-
CGRect fillCellRect = cellRect;
933-
934-
if (c == grid.cols - 1 || (c == grid.cols - 2 && (cell.textFlags & DRAW_WIDE))) {
935-
// Fill a little extra to the right if this is the last
936-
// column, and the frame size isn't exactly the same size
937-
// as the grid (due to smooth resizing, etc). This makes it
938-
// look less ugly and more consisten. See rectForRow:'s
939-
// implementation for extra comments.
940-
CGFloat extraWidth = rowRect.origin.x + rowRect.size.width - (cellRect.size.width + cellRect.origin.x);
941-
fillCellRect.size.width += extraWidth;
942-
}
943-
944-
CGContextSetFillColor(ctx, COMPONENTS(cell.bg));
945-
CGContextFillRect(ctx, fillCellRect);
946-
}
947-
948-
// Handle signs
949-
if (cell.sign) {
950-
CGRect signRect = cellRect;
951-
signRect.size.width *= 2;
952-
[cell.sign drawInRect:signRect
953-
fromRect:(NSRect){{0, 0}, cell.sign.size}
954-
operation:(cell.inverted ? NSCompositingOperationDifference : NSCompositingOperationSourceOver)
955-
fraction:1.0];
956-
}
957-
958-
// Insertion point (cursor)
959-
if (cell.insertionPoint.color && cell.insertionPoint.fraction) {
960-
float frac = cell.insertionPoint.fraction / 100.0;
961-
NSRect rect = cellRect;
962-
if (MMInsertionPointHorizontal == cell.insertionPoint.shape) {
963-
rect.size.height = cellSize.height * frac;
964-
} else if (MMInsertionPointVertical == cell.insertionPoint.shape) {
965-
rect.size.width = cellSize.width * frac;
966-
} else if (MMInsertionPointVerticalRight == cell.insertionPoint.shape) {
967-
rect.size.width = cellSize.width * frac;
968-
rect.origin.x += cellRect.size.width - rect.size.width;
969-
}
970-
rect = [self backingAlignedRect:rect options:NSAlignAllEdgesInward];
971-
972-
[[NSColor colorWithArgbInt:cell.insertionPoint.color] set];
973-
if (MMInsertionPointHollow == cell.insertionPoint.shape) {
974-
[NSBezierPath strokeRect:NSInsetRect(rect, 0.5, 0.5)];
975-
} else {
976-
NSRectFill(rect);
977-
}
978-
}
979-
9801065
// Text underline styles. We only allow one of them to be active.
9811066
// Note: We are not currently using underlineThickness or underlinePosition. Should fix to use them.
9821067
const CGFloat underlineY = 0.4*fontDescent; // Just a hard-coded value for now. Should fix to use underlinePosition.
@@ -1089,7 +1174,7 @@ - (void)drawRect:(NSRect)rect
10891174
if (hasStrikeThrough) {
10901175
// Second pass to render strikethrough. Unfortunately have to duplicate a little bit of code here to loop
10911176
// through the cells.
1092-
for (size_t c = 0; c < grid.cols; c++) {
1177+
for (int c = 0; c < grid.cols; c++) {
10931178
GridCell cell = *grid_cell(&grid, r, c);
10941179
CGRect cellRect = {{rowRect.origin.x + cellSize.width * c, rowRect.origin.y}, cellSize};
10951180
if (cell.textFlags & DRAW_WIDE)
@@ -1109,9 +1194,21 @@ - (void)drawRect:(NSRect)rect
11091194

11101195
}
11111196
}
1197+
};
1198+
1199+
// Render passes:
1200+
1201+
// 1. Draw background color and cursor rect.
1202+
drawAllRows(drawBackgroundAndCursorFunc);
1203+
1204+
// 2. Draw text.
1205+
// We need to do this in a separate pass in case some characters are taller than a cell. This
1206+
// could easily happen when we have composed characters (e.g. T゙̂⃗) that either goes below or above
1207+
// the cell boundary. We draw the background colors in 1st pass to make sure all the texts will
1208+
// be drawn on top of them. Also see redrawExpandRows which handles making such tall characters
1209+
// redraw/clip correctly.
1210+
drawAllRows(drawTextFunc);
11121211

1113-
CGContextRestoreGState(ctx);
1114-
}
11151212
if (thinStrokes) {
11161213
CGContextSetFontSmoothingStyle(ctx, originalSmoothingStyle);
11171214
}

src/MacVim/Miscellaneous.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ extern NSString *MMNonNativeFullScreenShowMenuKey;
6161
extern NSString *MMNonNativeFullScreenSafeAreaBehaviorKey;
6262
extern NSString *MMSmoothResizeKey;
6363
extern NSString *MMCmdLineAlignBottomKey;
64+
extern NSString *MMRendererClipToRowKey;
6465
extern NSString *MMAllowForceClickLookUpKey;
6566
extern NSString *MMUpdaterPrereleaseChannelKey;
6667

src/MacVim/Miscellaneous.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
NSString *MMNonNativeFullScreenSafeAreaBehaviorKey = @"MMNonNativeFullScreenSafeAreaBehavior";
5858
NSString *MMSmoothResizeKey = @"MMSmoothResize";
5959
NSString *MMCmdLineAlignBottomKey = @"MMCmdLineAlignBottom";
60+
NSString *MMRendererClipToRowKey = @"MMRendererClipToRow";
6061
NSString *MMAllowForceClickLookUpKey = @"MMAllowForceClickLookUp";
6162
NSString *MMUpdaterPrereleaseChannelKey = @"MMUpdaterPrereleaseChannel";
6263

0 commit comments

Comments
 (0)