Add proper underline and strikethrough support #1078
Conversation
Here are the results of the benchmarks I've run: Master
Underline + Strikethrough
Benchmark Script
All benchmarks with color support used a small patch which randomly generates SGR sequences instead of just changing color so underline and strikethrough is actually tested. |
As usual for your PRs, super awesome! I tested my font (Consolas) as well as another one that I like (Source Code Pro), I have two comments/questions:
IMO LibreOffice renders both fonts more beautifully, underline is closer to the text in Consolas and strikethrough is centered in Source Code Pro. Closer comparison: |
For the underline I'm taking the metrics that are provided by the font itself, so those should be okay in theory. However with the strikethrough I'm just taking the cell height and applying strike-through half way through the cell. It might be a better approach to take the font baseline and the ascent and drawing the strikethrough half way through there. Thanks for the quick feedback. :) EDIT: What I haven't thought about is the actual alignment of the underline with a width that is not 1. It looks to me like the Consolas underline is more than one pixel wide. Right now I just assumed that the position of the underline is the top corner of it. However if it would be the middle or even the bottom the underline of Consolas would be higher. I'll look into that again, it's probably somewhere in the freetype documentation. |
According to the docs: So I'm currently still doing it wrong because I'm positioning the underline relative to the baseline, of the underline bar's top. Not I can't find any specifics on strikethrough though. |
After looking a bit more into it, it seems like there is no "standard" way for rendering the strikethrough. So where exactly the strikethrough line is placed seems to vary significantly from terminal to terminal. From the ones I've looked at none seems perfect. Another thing I've noticed is that the underline position and thickness are always the same, no matter what font size. I'm not entirely sure if that's correct, because fonts with a huge underline offset (Hack has -4) will lead to the underline leaving the glyph box at small sizes. |
The underline raised a little higher with the latest commit, do you have some ideas about how to fix the underline being so low when the font size is very small? I'm just curious, given the benefits and that it works for usual font sizes I personally think it's fine to merge this even in the current state. This is font size of
|
I'll be honest here, the state this PR is in right now is pretty much completely unusable with some font/size combinations. And I have no idea how exactly freetype underline and position is supposed to work. I'll have to look more into drawing underline with freetype, maybe check out how some other applications do it. But as long as underline position/width is always the same with all font sizes, this will not work properly. |
Apparently the undreline position and thickness are specified in font units, not device pixels, so they need to be converted first. There also is an exact specification for strikethrough position and thickness using the I'll update this PR with a reworked version of this as soon as possible, so others can test it with their font setup. |
Underline has been reworked, however there's still one small caveat:
I'd love some feedback on the issue if underline should always be rendered. Strikethrough will hopefully be up tomorrow. :) |
Very cool, now the position of the underline stays relatively on the same place regardless of the font size. Also, a nice accident, using Consolas with my regular font size of 10 the underline perfectly matches the bottom of the box cursor, just like I wanted In fact, the perfect match is for font sizes from 1 to 12, and starting from font size 13 it goes out of sync by one pixel... but it doesn't matter. And this is Source Code Pro: |
I'd classify the first GIF as a bug. It might be up to freetype standards (not sure, I'd have to look into it), but the underline should never be even a single pixel below the cell height. Since the cell height is the full height of the line, any pixel below that is part of the next line and should never contain content from the previous one. It's probably just a rounding/edge case thing, thanks for always being so quick to test things. :) |
The current breakage on macos is intentional. I will need some time to test this with a VM. |
39fbf2a
to
a0270d9
@maximbaz To check if the font specifies an incorrect underline position/thickness or if there's some kind of off-by-one error, it would really help if you would give me the metrics for the situation where the underline goes one pixel (or more) beyond the block cursor's bottom line. Here's a patch you can apply to this PR (a0270d9) for printing the metrics to stdout: |
Happy to help The underline is pixel-perfect on font sizes [5-12, 18, 21] and is off on font sizes [13-17, 19-20]. Click
|
By the way, the position of strikeout is very good now, on a few fonts that I tested and on a wide range of font sizes |
Hmm that's odd. If I calculate the position myself from the metrics you've provided, I get a result that's still within the cell bounds. Unfortunately I'm also not able to reproduce it on my system which kinda makes this a bit hard to troubleshoot for me. When using consolas myself (size 20) it looks like this: It seems to be well within bounds. |
let y = ((start.line.0 as f32 + 1.) * metrics.line_height as f32 + metrics.descent | ||
- metrics.underline_position | ||
- metrics.underline_thickness / 2.) | ||
.round() as u32; |
maximbaz
Feb 18, 2018
Contributor
Just out of curiosity I replaced .round()
with .floor()
here and got these results:
- pixel perfect underline on font sizes [8-18, 21]
- underline beyond cell height on font sizes [5-7, 19-20]
This is obviously not a solution, but it got me wondering: metrics.*
properties are all rounded in font/src/ft/mod.rs
, here we have some calculations based on already rounded values and then round again, can't this introduce some increasing calculation error? What if metrics.*
were not rounded in font/src/ft/mod.rs
, but left as f32
, and here we had rounding only once, in the end of all calculations?
Just out of curiosity I replaced .round()
with .floor()
here and got these results:
- pixel perfect underline on font sizes [8-18, 21]
- underline beyond cell height on font sizes [5-7, 19-20]
This is obviously not a solution, but it got me wondering: metrics.*
properties are all rounded in font/src/ft/mod.rs
, here we have some calculations based on already rounded values and then round again, can't this introduce some increasing calculation error? What if metrics.*
were not rounded in font/src/ft/mod.rs
, but left as f32
, and here we had rounding only once, in the end of all calculations?
maximbaz
Feb 18, 2018
Contributor
In case there are multiple Consolas fonts with different metrics, I use this one - try, maybe you can reproduce with it 🙂
In case there are multiple Consolas fonts with different metrics, I use this one - try, maybe you can reproduce with it
chrisduerr
Feb 18, 2018
Author
Collaborator
I think rounding multiple times should be fine because these metrics should never be float values. The underline position for example shouldn't be 11.2
pixels below the baseline because the concept of 0.2
pixels doesn't exist. And after we make use of the metrics we have to round again because we need u32
s, so it's either floor, ceil or round to get valid integers.
I think that approach is correct, however if someone else knows more, please correct me.
Reproducing your issue will probably be hard for me because of potential DPI differences. I've used the consolas-fonts
package to test though, so they might be different.
The question is just if my "algorithm" is correct really. I can't see anything wrong with it and it should be fine with the values you've supplied. If the algorithm is correct, but the font still specifies an offset that is out of bounds, I think alacritty should handle it.
I'm gonna go ahead and implement a bounds check which will make sure that whenever the underline is out of bounds, the underline will be moved up so it's exactly within bounds (using the thickness provided with the font). This should fix the issue and also help with potentially broken font metrics.
However if my approach to this calculation is wrong and maybe rounding shouldn't take place in the metrics, we should still fix this instead of just relying on a bounds check to catch off-by-one errors.
I think rounding multiple times should be fine because these metrics should never be float values. The underline position for example shouldn't be 11.2
pixels below the baseline because the concept of 0.2
pixels doesn't exist. And after we make use of the metrics we have to round again because we need u32
s, so it's either floor, ceil or round to get valid integers.
I think that approach is correct, however if someone else knows more, please correct me.
Reproducing your issue will probably be hard for me because of potential DPI differences. I've used the consolas-fonts
package to test though, so they might be different.
The question is just if my "algorithm" is correct really. I can't see anything wrong with it and it should be fine with the values you've supplied. If the algorithm is correct, but the font still specifies an offset that is out of bounds, I think alacritty should handle it.
I'm gonna go ahead and implement a bounds check which will make sure that whenever the underline is out of bounds, the underline will be moved up so it's exactly within bounds (using the thickness provided with the font). This should fix the issue and also help with potentially broken font metrics.
However if my approach to this calculation is wrong and maybe rounding shouldn't take place in the metrics, we should still fix this instead of just relying on a bounds check to catch off-by-one errors.
maximbaz
Feb 18, 2018
Contributor
I added this to your patch:
println!("CALC POS F32: {}", f32::from(face.ft_face.underline_position()) * x_scale / 64.);
println!("CALC THI F32: {}", f32::from(face.ft_face.underline_thickness()) * x_scale / 64.);
Let's look at a random output:
-----------
CELL_HEIGHT: 44
DESCENT: -10
UNDERLINE POS: -482
UNDERLINE THI: 144
X SCALE: 1.1733398
CALC POS F32: -8.836716
CALC THI F32: 2.6400146
CALC POS: -9
CALC THI: 3
-----------
What I was saying is that because F32 values can have any fractional part, the following hypothetical example can happen:
CALC_POS_F32 = 7.49
CALC_THI_F32 = 2.49
(CALC_POS_F32.round() + CALC_THI_F32.round() / 2.0).round() == (7 + 2 / 2).round() == 8
(CALC_POS_F32 + CALC_THI_F32 / 2.0).round() == (8.735).round() == 9
Let me know if this makes sense, I'll try the new bounds check in meantime.
I added this to your patch:
println!("CALC POS F32: {}", f32::from(face.ft_face.underline_position()) * x_scale / 64.);
println!("CALC THI F32: {}", f32::from(face.ft_face.underline_thickness()) * x_scale / 64.);
Let's look at a random output:
-----------
CELL_HEIGHT: 44
DESCENT: -10
UNDERLINE POS: -482
UNDERLINE THI: 144
X SCALE: 1.1733398
CALC POS F32: -8.836716
CALC THI F32: 2.6400146
CALC POS: -9
CALC THI: 3
-----------
What I was saying is that because F32 values can have any fractional part, the following hypothetical example can happen:
CALC_POS_F32 = 7.49
CALC_THI_F32 = 2.49
(CALC_POS_F32.round() + CALC_THI_F32.round() / 2.0).round() == (7 + 2 / 2).round() == 8
(CALC_POS_F32 + CALC_THI_F32 / 2.0).round() == (8.735).round() == 9
Let me know if this makes sense, I'll try the new bounds check in meantime.
chrisduerr
Feb 18, 2018
•
Author
Collaborator
Yeah I understand what you mean. But in my opinion it doesn't make any sense to have a fractional underline position/thickness. It should always be an integer offset.
In fact the .round()
and .floor()
should not make any difference because every part of the calculation (descent, line_height, underline pos/thickness) should all be integers.
Yeah I understand what you mean. But in my opinion it doesn't make any sense to have a fractional underline position/thickness. It should always be an integer offset.
In fact the .round()
and .floor()
should not make any difference because every part of the calculation (descent, line_height, underline pos/thickness) should all be integers.
maximbaz
Feb 18, 2018
Contributor
I don't see any problem with your algorithm either, and just out of curiosity I verified that simply removing .round()
when calculating CALC POS
and CALC THI
doesn't solve my particular problem, so I agree, let's keep the code as it is. Bounds check on the other hand is solving the problem well 🙂
I don't see any problem with your algorithm either, and just out of curiosity I verified that simply removing .round()
when calculating CALC POS
and CALC THI
doesn't solve my particular problem, so I agree, let's keep the code as it is. Bounds check on the other hand is solving the problem well
|
||
let (y, height) = match flag { | ||
cell::Flags::UNDERLINE => { | ||
// Get the baseline positon and offset it down by (-) underline position |
maximbaz
Feb 18, 2018
Contributor
positon -> position
positon -> position
chrisduerr
Feb 18, 2018
Author
Collaborator
Thanks :)
Thanks :)
I've added the bounds check, would be interested to see if that fixes it for you @maximbaz. I'd still love to know why it's rendered outside in the first place. |
With the last commit the underline is never exceeding the cell and thus always matches the box cursor perfectly By the way, my dpi value is 210, so you can also try |
I've tried to reproduce it with It's calculated in line 556 of |
I forgot to ask you what the line number is but based on the That If the line number is only These numbers seem a bit odd to me. |
The line number is
|
Yeah so that doesn't make any sense at all then. Clearly if the line number is If you have another line below one with underline it actually does intersect with it, right? It's not like the block cursor is just not big enough? |
I believe yes, but it's hard to tell, which symbol will fill the entire cell besides block cursor? And with the latest commit in this PR: It is curious to investigate, but I feel we should leave this with the approach taken in the last commit |
The best character to render for filling the whole block is the box cursor. The unicode character for it is defined here: |
|
This makes use of the new rectangle rendering methods used to display the colored visual bell to add proper underline and strikethrough support to Alacritty.
98a8000
to
b0c90c3
With a bit of work I was able to rebase this branch back on master and make it work again. It seems to be performing fairly well in the minimal tests I've done, so I don't expect any regressions from the state it was in when it was put on hold. |
@dm1try That's not with an offset of 0, a glyph offset of 0, a padding of 0 and 1.0 DPI though, right? |
I used default settings(offsets were commented) and I've tried with explicit set to
according to DPI, I use default to device scaling. though DPR is 2 on retina display:
|
I haven't considered DPI at all yet because back then it was a bit different. I'll update this PR asap with the remaining issues resolved and refactored code, hopefully that should fix it. |
btw, unrelated to this issue, but as you can see on the screenshot above the default letter spacing in alacritty is different from |
This refactors the code necessary to generate the underlines and srikeouts into a different file, cleaning up some of the code in the process of doing so. This also switches out some of the font metrics to use cell metrics instead, which has the advantage of including additional information like the offset specified in the configuration file.
As far as I'm concerned, I've addressed all remaining issues except for Windows support. So in theory it should work on macOS now if the metrics are calculated correctly. Would be appreciated if you could test again @dm1try, hopefully that resolves your problem. |
Also it looks like rusttype provides neither underline, nor strikeout metrics. As a result of that, I'm not sure if it makes sense at this point to support underline/strikeout at all on Windows at this point. @zacps What do you think about keeping the old hack in place for Window until fontkit has been landed? It seems like fontkit at least offers the underline metrics, maybe we can even upstream strikeout support. Otherwise our custom strikeout which I've had to implement for macOS would do just fine, the important part is mainly the underline. |
@chrisduerr thanks, it works now! (checked on both displays) |
Fontkit for Windows is basically done, we could ship it for Windows while I work on Linux fixes. I guess it depends how keen you are to get this through? I'd like to say fontkit would be done in a couple of weeks but unfortunately life doesn't work like that. |
@zacps I'd like to get this merged, then have conPTY as the next significant PR and work my way torwards fontkit. With some fixes to URL launching and similar stuff in between. I'm totally fine with fixing up fontkit myself on linux, that shouldn't be any problem. I can help out at least a little bit to speed things up. My idea was to get this PR into 0.2.5 and potentially release 0.2.6 with fontkit (at least as an option on macOS/linux, probably the default on Windows). So for me it doesn't make much sense to try and spin up some mediocre underline/strikeout support for Windows now, when the next release is going to have proper support for it anyways. Just so we don't have to do things twice. The old and hacky way to do URLs can still stay in place for Windows, that shouldn't be a problem, so there's not gonna be any regression. |
So I started writing the code for using the old fallback behavior just for Windows and then I realized that I'm just making life hard on myself. Coming up with some okay-ish metrics for underline and strikeout really isn't difficult, so the much, much easier solution was just to implement these metrics. No need to make it unnecessarily hard and ugly. Would appreciate some testing though @zacps, since I don't have access to Windows right now. There's no scientific background for the way I made these metrics up, other than them looking decent on linux. So let me know if that works out for you too! |
Unfortunately I also don't have access to Windows until the new year, but I can take a look at it then. |
I copied the echo command that @dm1try used above to test. Strikethrough doesn't seem to be working, but underline is. Adding a reset escape code Let me know if there's any other information or testing I can provide. |
@cole-h Thanks a ton for testing this. The issue with resetting is 'intentional'. I simply didn't add the reset code at the end because my prompt adds it automatically. The only question left really is if strikeout doesn't work because Window is messing with things or if the metrics are wrong. Could you try applying this patch? diff --git a/src/renderer/lines.rs b/src/renderer/lines.rs
index 556dcb0..f90972b 100644
--- a/src/renderer/lines.rs
+++ b/src/renderer/lines.rs
@@ -118,9 +118,7 @@ fn create_rect(
let width = end_x - start_x;
let (position, height) = match flag {
- Flags::UNDERLINE => (metrics.underline_position, metrics.underline_thickness),
- Flags::STRIKEOUT => (metrics.strikeout_position, metrics.strikeout_thickness),
- _ => unimplemented!("Invalid flag for cell line drawing specified"),
+ _ => (metrics.underline_position, metrics.underline_thickness),
};
let cell_bottom = (start.line.0 as f32 + 1.) * size.cell_height; What it does is simply ignore the strikeout metrics and always render underlines for both strikeout and underlines. So if this patch doesn't underline the |
Thanks, so I think this is good to do then. I'll do a final code review to make sure everything's clean and working properly and then it'll hopefully be merged. Thanks a ton for the help again @cole-h! |
Thanks for implementing and updating. However, when testing this, I have to say the underline position is a bit unfortunate with the terminus font. The upper terminal shows an older master version (cf9d94e) with a patch I applied as per #31 (comment) and the lower terminal is from current master unpatched: |
@FichteFoll I believe you're absolutely correct and there is actually a bug in the computation of the line metrics. I've created a PR here, please let me know if that fixes it. Also feel free to just open a new issue in the future if you're suspecting that there might be a bug. Feedback is always appreciated! |
Wasn't sure about it being a bug or a problem on my part, so I presumed commenting on the PR that introduced the change was the most appropriate. |
Support for strikethrough has been added by inserting and removing a
STRIKE_THROUGH
flag on the cell.Now all strikethrough and underline drawing is also done through the
rectangle renderer. So no glyphs are used to render underlines and
strikethrough.
The position is taken from the font metrics and should be accurate for
linux, however is not yet tested on macos.
It works by checking the underline state for each cell and then drawing
from the start until the last position whenever an underline ended. This
adds a few checks even if no underline is rendered but I was not able to
measure any significant performance impact.
Fixes #806.
Fixes #31.
Demo:

TODO: