From 882944f1463246c7be1a47e12b2f94165ce84437 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Wed, 14 Sep 2022 20:09:11 -0700 Subject: [PATCH] Support strikethrough/underdouble/underdashed/underdotted styles Add support for all the missing text styles for MacVim for Vim parity. For strikethrough, this needed to be done as a second pass to make sure they get drawn on top of the text. This is necessary because currently the logic buffers texts up before dispatching them later in a line, so it's just easier to loop through the line a second time if we detected strikethrough. For the strikethrough position, we simply use the half of xheight which seems to work best in looking good. For underdouble, the logic is a little tricky because sometimes we don't have space below the line. When that's the case, simply draw the second line on top of the first line. For underdotted, need to do something smart to space out the dots. When the width is divisible by 2, they get spaced out evenly. If they are not, try to make it work if divisible by 3. If that's not the case, we just readjust the size of dot/gap a little bit to make it fit, even though now we have non-integer sizes (from experimentation, the antialising works well enough that it's not too jarring). Also fix rendering of undercurl to work for double-width characters as well. Note that underdouble/underdotted/underdashed are not supported in regular gVim yet, and so I had to add the ifdef for those in gui.c. These may cause merge conflicts later which should be easily resolved. Known issue 1: Note that currently underline is not respecting the font's underline thickness and position. We always use a thickness of 1 pt, and hard-code a 0.4*descent position, which are not great. Thickness in particular should scale with the font size. They should be fixed in a future commit. Known issue 2: There are some current clipping bugs in the renderer. This is because the line height returned by NSLayoutManager is sometimes smaller than ascent+descent+leading, *and* MMCoreTextView for some reason takes the `ceil(descent)` (presumably to make the rendering grid-aligned). This should be fixed later. Fix #1034 --- src/MacVim/MMCoreTextView.h | 4 +- src/MacVim/MMCoreTextView.m | 133 ++++++++++++++++++++++++++++++++---- src/gui.c | 9 ++- src/gui.h | 14 +++- 4 files changed, 142 insertions(+), 18 deletions(-) diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index ba31aac84f..470425336a 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -27,7 +27,9 @@ // From NSTextView NSSize insetSize; - float fontDescent; + CGFloat fontDescent; + CGFloat fontAscent; + CGFloat fontXHeight; BOOL antialias; BOOL ligatures; BOOL thinStrokes; diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 4336d41069..5192dec940 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -36,14 +36,18 @@ // TODO: What does DRAW_TRANSP flag do? If the background isn't drawn when // this flag is set, then sometimes the character after the cursor becomes // blank. Everything seems to work fine by just ignoring this flag. -#define DRAW_TRANSP 0x01 /* draw with transparent bg */ -#define DRAW_BOLD 0x02 /* draw bold text */ -#define DRAW_UNDERL 0x04 /* draw underline text */ -#define DRAW_UNDERC 0x08 /* draw undercurl text */ -#define DRAW_ITALIC 0x10 /* draw italic text */ +#define DRAW_TRANSP 0x01 // draw with transparent bg +#define DRAW_BOLD 0x02 // draw bold text +#define DRAW_UNDERL 0x04 // draw underline text +#define DRAW_UNDERC 0x08 // draw undercurl text +#define DRAW_ITALIC 0x10 // draw italic text #define DRAW_CURSOR 0x20 -#define DRAW_WIDE 0x80 /* draw wide text */ -#define DRAW_COMP 0x100 /* drawing composing char */ +#define DRAW_STRIKE 0x40 // draw strikethrough text +#define DRAW_UNDERDOUBLE 0x80 // draw double underline +#define DRAW_UNDERDOTTED 0x100 // draw dotted underline +#define DRAW_UNDERDASHED 0x200 // draw dashed underline +#define DRAW_WIDE 0x1000 // (MacVim only) draw wide text +#define DRAW_COMP 0x2000 // (MacVim only) drawing composing char #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_13 typedef NSString * NSAttributedStringKey; @@ -431,6 +435,8 @@ - (void)setFont:(NSFont *)newFont font = [newFont retain]; } fontDescent = ceil(CTFontGetDescent((CTFontRef)font)); + fontAscent = CTFontGetAscent((CTFontRef)font); + fontXHeight = CTFontGetXHeight((CTFontRef)font); // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will // only render at integer sizes. Hence, we restrict the cell width to @@ -884,6 +890,9 @@ - (void)drawRect:(NSRect)rect CGContextSetBlendMode(ctx, kCGBlendModeCopy); [lineString deleteCharactersInRange:NSMakeRange(0, lineString.length)]; }; + + BOOL hasStrikeThrough = NO; + for (size_t c = 0; c < grid.cols; c++) { GridCell cell = *grid_cell(&grid, r, c); CGRect cellRect = {{rowRect.origin.x + cellSize.width * c, rowRect.origin.y}, cellSize}; @@ -945,19 +954,90 @@ - (void)drawRect:(NSRect)rect } } - // Text underline styles - if (cell.textFlags & DRAW_UNDERL) { - CGRect rect = CGRectMake(cellRect.origin.x, cellRect.origin.y+0.4*fontDescent, cellRect.size.width, 1); - CGContextSetFillColor(ctx, COMPONENTS(cell.sp)); - CGContextFillRect(ctx, rect); - } else if (cell.textFlags & DRAW_UNDERC) { - const float x = cellRect.origin.x, y = cellRect.origin.y+1, w = cellSize.width, h = 0.5*fontDescent; + // Text underline styles. We only allow one of them to be active. + // Note: We are not currently using underlineThickness or underlinePosition. Should fix to use them. + const CGFloat underlineY = 0.4*fontDescent; // Just a hard-coded value for now. Should fix to use underlinePosition. + if (cell.textFlags & DRAW_UNDERC) { + const CGFloat x = cellRect.origin.x, y = cellRect.origin.y+1, w = cellSize.width, h = 0.5*fontDescent; CGContextMoveToPoint(ctx, x, y); CGContextAddCurveToPoint(ctx, x+0.25*w, y, x+0.25*w, y+h, x+0.5*w, y+h); CGContextAddCurveToPoint(ctx, x+0.75*w, y+h, x+0.75*w, y, x+w, y); + if (cell.textFlags & DRAW_WIDE) { + // Need to draw another set for double-width characters + const CGFloat x2 = x + cellSize.width; + CGContextAddCurveToPoint(ctx, x2+0.25*w, y, x2+0.25*w, y+h, x2+0.5*w, y+h); + CGContextAddCurveToPoint(ctx, x2+0.75*w, y+h, x2+0.75*w, y, x2+w, y); + } + CGContextSetRGBStrokeColor(ctx, RED(cell.sp), GREEN(cell.sp), BLUE(cell.sp), ALPHA(cell.sp)); + CGContextStrokePath(ctx); + } + else if (cell.textFlags & DRAW_UNDERDASHED) { + const CGFloat dashLengths[] = {cellSize.width / 4, cellSize.width / 4}; + + const CGFloat x = cellRect.origin.x; + const CGFloat y = cellRect.origin.y+underlineY; + CGContextMoveToPoint(ctx, x, y); + CGContextAddLineToPoint(ctx, x + cellRect.size.width, y); CGContextSetRGBStrokeColor(ctx, RED(cell.sp), GREEN(cell.sp), BLUE(cell.sp), ALPHA(cell.sp)); + CGContextSetLineDash(ctx, 0, dashLengths, 2); CGContextStrokePath(ctx); } + else if (cell.textFlags & DRAW_UNDERDOTTED) { + // Calculate dot size to use. Normally, just do 1-pixel dots/gaps, since the line is one pixel thick. + CGFloat dotSize = 1, gapSize = 1; + if (fmod(cellSize.width, 2) != 0) { + // Width is not even number, so spacing them would look weird. Find another way. + if (fmod(cellSize.width, 3) == 0) { + // Width is divisible by 3, so just make the gap twice as long so they can be spaced out. + dotSize = 1; + gapSize = 2; + } + else { + // Not disible by 2 or 3. Just Re-calculate dot size so be slightly larger than 1 so we can exactly + // equal number of dots and gaps. This does mean we have a non-integer size, so we are relying + // on anti-aliasing here to help this not look too bad, but it will still look slightly blurry. + dotSize = cellSize.width / (ceil(cellSize.width / 2) * 2); + gapSize = dotSize; + } + } + const CGFloat dashLengths[] = {dotSize, gapSize}; + + const CGFloat x = cellRect.origin.x; + const CGFloat y = cellRect.origin.y+underlineY; + CGContextMoveToPoint(ctx, x, y); + CGContextAddLineToPoint(ctx, x + cellRect.size.width, y); + CGContextSetRGBStrokeColor(ctx, RED(cell.sp), GREEN(cell.sp), BLUE(cell.sp), ALPHA(cell.sp)); + CGContextSetLineDash(ctx, 0, dashLengths, 2); + CGContextStrokePath(ctx); + } + else if (cell.textFlags & DRAW_UNDERDOUBLE) { + CGRect rect = CGRectMake(cellRect.origin.x, cellRect.origin.y+underlineY, cellRect.size.width, 1); + CGContextSetFillColor(ctx, COMPONENTS(cell.sp)); + CGContextFillRect(ctx, rect); + + // Draw second underline + if (underlineY - 3 < 0) { + // Not enough fontDescent to draw another line below, just draw above. This is not the desired + // solution but works. + rect = CGRectMake(cellRect.origin.x, cellRect.origin.y+underlineY + 3, cellRect.size.width, 1); + } else { + // Nominal situation. Just a second one below first one. + rect = CGRectMake(cellRect.origin.x, cellRect.origin.y+underlineY - 3, cellRect.size.width, 1); + } + CGContextSetFillColor(ctx, COMPONENTS(cell.sp)); + CGContextFillRect(ctx, rect); + } else if (cell.textFlags & DRAW_UNDERL) { + CGRect rect = CGRectMake(cellRect.origin.x, cellRect.origin.y+underlineY, cellRect.size.width, 1); + CGContextSetFillColor(ctx, COMPONENTS(cell.sp)); + CGContextFillRect(ctx, rect); + } + + // Text strikethrough + // We delay the rendering of strikethrough and only do it as a second-pass since we want to draw them on top + // of text, and text rendering is currently delayed via flushLineString(). + if (cell.textFlags & DRAW_STRIKE) { + hasStrikeThrough = YES; + } // Draw the actual text if (cell.string) { @@ -981,6 +1061,31 @@ - (void)drawRect:(NSRect)rect } flushLineString(); [lineString release]; + + if (hasStrikeThrough) { + // Second pass to render strikethrough. Unfortunately have to duplicate a little bit of code here to loop + // through the cells. + for (size_t c = 0; c < grid.cols; c++) { + GridCell cell = *grid_cell(&grid, r, c); + CGRect cellRect = {{rowRect.origin.x + cellSize.width * c, rowRect.origin.y}, cellSize}; + if (cell.textFlags & DRAW_WIDE) + cellRect.size.width *= 2; + if (cell.inverted) { + cell.bg ^= 0xFFFFFF; + cell.fg ^= 0xFFFFFF; + cell.sp ^= 0xFFFFFF; + } + + // Text strikethrough + if (cell.textFlags & DRAW_STRIKE) { + CGRect rect = CGRectMake(cellRect.origin.x, cellRect.origin.y + fontDescent + fontXHeight / 2, cellRect.size.width, 1); + CGContextSetFillColor(ctx, COMPONENTS(cell.sp)); + CGContextFillRect(ctx, rect); + } + + } + } + CGContextRestoreGState(ctx); } if (thinStrokes) { diff --git a/src/gui.c b/src/gui.c index bd50be9a7a..2331c15ecf 100644 --- a/src/gui.c +++ b/src/gui.c @@ -2527,7 +2527,14 @@ gui_outstr_nowrap( if (hl_mask_todo & HL_UNDERCURL) draw_flags |= DRAW_UNDERC; - // TODO: HL_UNDERDOUBLE, HL_UNDERDOTTED, HL_UNDERDASHED + // MacVim note: underdouble/underdotted/underdashed are not implemented in Vim yet. + // These are MacVim-only for now. + if (hl_mask_todo & HL_UNDERDOUBLE) + draw_flags |= DRAW_UNDERDOUBLE; + if (hl_mask_todo & HL_UNDERDOTTED) + draw_flags |= DRAW_UNDERDOTTED; + if (hl_mask_todo & HL_UNDERDASHED) + draw_flags |= DRAW_UNDERDASHED; // Do we strikethrough the text? if (hl_mask_todo & HL_STRIKETHROUGH) diff --git a/src/gui.h b/src/gui.h index 2575245567..469ebdd268 100644 --- a/src/gui.h +++ b/src/gui.h @@ -128,8 +128,18 @@ #endif #define DRAW_CURSOR 0x20 // drawing block cursor (win32) #define DRAW_STRIKE 0x40 // strikethrough -#define DRAW_WIDE 0x80 // drawing wide char (MacVim) -#define DRAW_COMP 0x100 // drawing composing char (MacVim) +// MacVim note: underdouble/underdotted/underdashed are not implemented in Vim yet. +// These are MacVim-only for now. +// IMPORTANT: If resolving a merge conflict when merging from upstream, if Vim decided +// to use different values for these constants, MMCoreTextView.m would need +// to be updated to reflect them as well, or the renderer won't understand +// these values. +#define DRAW_UNDERDOUBLE 0x80 // draw double underline +#define DRAW_UNDERDOTTED 0x100 // draw dotted underline +#define DRAW_UNDERDASHED 0x200 // draw dashed underline + +#define DRAW_WIDE 0x1000 // drawing wide char (MacVim) +#define DRAW_COMP 0x2000 // drawing composing char (MacVim) // For our own tearoff menu item #define TEAR_STRING "-->Detach"