Repository: microsoft/terminal (language: cpp)

Issue:
Configuration for undercurl vertical offset

![image](https://github.com/microsoft/terminal/assets/6503931/f4933dd1-be61-401a-b773-7c56c4e8f8c0)

Undercurl is colliding with the bottom of the font. Requesting a configuration to apply a y-offset from the default vertical placement.

Here are some other-term links that may prevent having to think it through from scratch.
https://github.com/kovidgoyal/kitty/issues/853
https://github.com/microsoft/cascadia-code/issues/395
https://wezfurlong.org/wezterm/config/lua/config/underline_position.html



Discussion / hints:
WezTerm w/ default settings.

![image](https://github.com/microsoft/terminal/assets/6503931/6117cae6-aa1a-4600-a081-f4e00a059ccb)

Mate-Terminal
![image](https://github.com/microsoft/terminal/assets/6503931/3b8e4821-0654-41d1-861d-60376f446c55)


I'd like to reproduce the issue first, before adding any settings. It could be a bug in our renderer after all. Can you please tell me the following?
* Your font family
* Font size
* Line height, if you set it
* Display scale (the one you set in the Windows Settings app)
I'm not sure how to check the font size once I change it. Opening a new session, I know that it's 10pt Hack Nerd Font Mono. Here's a look at that. Line height is default. Display scale is 100%. Grayscale and ClearType antialiasing look identical (literally identical; I tried a restart to make sure).
![image](https://github.com/microsoft/terminal/assets/6503931/e4193f6c-d8b8-464f-90d1-8628f96c2baa)
```json
{
    "$help": "https://aka.ms/terminal-documentation",
    "$schema": "https://aka.ms/terminal-profiles-schema-preview",
    "actions": 
    [
        {
            "command": 
            {
                "action": "splitPane",
                "split": "auto",
                "splitMode": "duplicate"
            },
            "id": "User.splitPane.A6751878",
            "keys": "alt+shift+d"
        },
        {
            "command": "toggleFocusMode",
            "id": "User.toggleFocusMode",
            "keys": "ctrl+shift+f"
        },
        {
            "command": "unbound",
            "keys": "ctrl+v"
        }
    ],
    "confirmCloseAllTabs": false,
    "copyFormatting": "none",
    "copyOnSelect": true,
    "defaultProfile": "{e2a4882f-638c-41a2-86c2-2c88bfe51b07}",
    "experimental.detectURLs": false,
    "launchMode": "maximized",
    "newTabMenu": 
    [
        {
            "type": "remainingProfiles"
        }
    ],
    "profiles": 
    {
        "defaults": 
        {
            "adjustIndistinguishableColors": "always",
            "cursorShape": "underscore",
            "font": 
            {
                "face": "Hack Nerd Font Mono",
                "size": 10
            },
            "padding": "0",
            "scrollbarState": "hidden"
        },
        "list": 
        [
            {
                "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
                "elevate": false,
                "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
                "hidden": false,
                "name": "Windows PowerShell"
            },
            {
                "adjustIndistinguishableColors": "indexed",
                "backgroundImage": "C:\\Users\\epramyl\\OneDrive - Ericsson\\Pictures\\pink_floyd_dsotm.jpg",
                "backgroundImageOpacity": 0.23,
                "bellStyle": "none",
                "closeOnExit": "graceful",
                "colorScheme": "Tango Dark",
                "commandline": "%SystemRoot%\\System32\\cmd.exe",
                "cursorShape": "filledBox",
                "experimental.rightClickContextMenu": true,
                "font": 
                {
                    "face": "Hack Nerd Font Mono",
                    "size": 10
                },
                "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
                "hidden": false,
                "historySize": 0,
                "name": "Command Prompt",
                "snapOnInput": false,
                "suppressApplicationTitle": true,
                "useAcrylic": false
            },
            {
                "guid": "{2c4de342-38b7-51cf-b940-2309a097f518}",
                "hidden": true,
                "name": "Ubuntu",
                "source": "Windows.Terminal.Wsl"
            },
            {
                "guid": "{b453ae62-4e3d-5e58-b989-0a998ec441b8}",
                "hidden": false,
                "name": "Azure Cloud Shell",
                "source": "Windows.Terminal.Azure"
            },
            {
                "guid": "{51855cb2-8cce-5362-8f54-464b92b32386}",
                "hidden": false,
                "name": "Ubuntu",
                "source": "CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc"
            },
            {
                "adjustIndistinguishableColors": "indexed",
                "altGrAliasing": true,
                "antialiasingMode": "cleartype",
                "bellStyle": "none",
                "closeOnExit": "automatic",
                "colorScheme": "Tango Dark",
                "commandline": "c:\\cygwin64\\bin\\bash.exe --login -i",
                "cursorShape": "filledBox",
                "experimental.retroTerminalEffect": false,
                "font": 
                {
                    "face": "Hack Nerd Font Mono",
                    "size": 11,
                    "weight": "normal"
                },
                "guid": "{95cbe99e-4035-4bc9-90a7-a7675ddb0d16}",
                "hidden": false,
                "historySize": 0,
                "icon": "C:\\cygwin64\\usr\\share\\icons\\hicolor\\256x256\\apps\\mintty.png",
                "name": "Cygwin Bash",
                "opacity": 100,
                "snapOnInput": false,
                "startingDirectory": "%USERPROFILE%",
                "tabTitle": "Cygwin Bash",
                "useAcrylic": false
            },
            {
                "adjustIndistinguishableColors": "indexed",
                "altGrAliasing": true,
                "antialiasingMode": "grayscale",
                "backgroundImage": "C:\\Users\\epramyl\\OneDrive - Ericsson\\Pictures\\pink_floyd_dsotm.jpg",
                "backgroundImageOpacity": 0.23,
                "bellStyle": "none",
                "closeOnExit": "graceful",
                "colorScheme": "Tango Dark",
                "commandline": "C:\\Windows\\System32\\OpenSSH\\ssh.exe -t 100.87.168.59",
                "cursorShape": "filledBox",
                "experimental.rightClickContextMenu": true,
                "font": 
                {
                    "face": "Hack Nerd Font Mono",
                    "size": 10
                },
                "guid": "{e2a4882f-638c-41a2-86c2-2c88bfe51b07}",
                "hidden": false,
                "historySize": 0,
                "icon": "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png",
                "name": "VDI",
                "snapOnInput": false,
                "startingDirectory": "%USERPROFILE%",
                "suppressApplicationTitle": true,
                "tabTitle": "VDI",
                "useAcrylic": false
            }
        ]
    },
    "rendering.graphicsAPI": "direct3d11",
    "schemes": [],
    "startOnUserLogin": true,
    "themes": [],
    "useAcrylicInTabRow": false,
    "wordDelimiters": " '\",=[]$*{}<>()^#!`;\t:\u2502"
}
```
The collision/legibility is definitely a function of font size. But the top of the curl is definitely abutting the bottom of the font. There needs to be at least some kind of small spacing.
![image](https://github.com/microsoft/terminal/assets/6503931/e4e177a5-8410-4c22-9980-cc76723d6009)

If I super-size, I can a bit of separation.
![image](https://github.com/microsoft/terminal/assets/6503931/96aa7406-4b97-4935-9e85-80e7e4b792df)

I think what I would like to see is a minimum integer number of pixels separation, no matter the font size. Even 1 pixel would be great. It would also be nice to be able to specify the pk-2-pk height of the curl, but that's just a nice to have.
Note that the collision is not as bad if I can set the undercurl color to be different than the font color. And I do have that set in nvim. But there is some kind of problem such that color works when not in tmux, but does not work in tmux (EXCEPT in MateTerminal it does work??? You can see that in the MateTerm grab above.). One day, I'll figure it out. But for now...
I think there are two problems:
* The curly underline is 1px too high up which causes it to overlap with the text. Mate, WezTerm, and Windows Terminal all draw a 3px tall wave, but we're the only one which positions it this high up.
* We currently only do vertical anti-aliasing. This looks good on HiDPI displays and large font sizes, but at low DPIs and small font sizes it looks ugly because it's too "pixel-y". I didn't do this because I was too lazy to figure out the math for how to calculate an arbitrary point-sine-wave distance. 🙈

In other words:
* Fix the vertical position of the curly underline
* Add horizontal anti-aliasing
After spending way too much time in desmos's graphing calculator I realized that I can calculate the distance to the tangent of the sin curve quite easily while being a sufficient approximation. The tangent for `sin(x)` is `cos(x)`, its angle is `atan(cos(x))` and the height of a right triangle with such an angle is:

```math
f(x,y) = \left|y-\sin\left(x\right)\right| \cdot \cos\left(\arctan\left(\cos\left(x\right)\right)\right)
```

That's such a fantastic coincidence because `cos(atan(cos(x)))` can be expressed not just using `sin(x)` which we already need anyway, but also using a reverse square root which is super cheap:

```math
f(x,y) = \left|y-\sin\left(x\right)\right| \cdot {1 \over \sqrt{2-sin(x)^2}}
```

This is what we already have:
```hlsl
float s = sin(data.position.x * frequency);
float d = abs(curlyLineHalfHeight - data.texcoord.y - s * amplitude);
```

and all that was missing was a tiny `rsqrt` at the end lol:
```hlsl
float s = sin(data.position.x * frequency);
float d = abs(curlyLineHalfHeight - data.texcoord.y - s * amplitude) * rsqrt(2 - s * s);
```
> We currently only do vertical anti-aliasing. This looks good on HiDPI displays and large font sizes, but at low DPIs and small font sizes it looks ugly because it's too "pixel-y".

IMO The sine wave does not need antialiasing even at a few pixels scale since it is quite symmetrical. Antialiasing only blurs it (this is just as ugly as using vertical AA for a single underline).

<details><summary>Here are examples</summary>
 of aliased wavy underlines (first with aliased glyphs and the second with AA glyph rendering)

Aliased glyphs:

https://github.com/microsoft/terminal/assets/11535558/538b4673-4fe9-4d3d-a4af-7640893c55ed

Antialiased glyphs:

https://github.com/microsoft/terminal/assets/11535558/36a23fe7-1519-4eb3-9881-0c700093f7f9

You can play with it using built artifacts https://github.com/directvt/vtm/actions/runs/9722846266 (or just build https://github.com/o-sdn-o/vtm/tree/gui-bridge) with the following config in `~/.config/vtm/settings.xml`:

```xml
<config>
    <gui>
        <antialiasing = off />
        <cellheight = 20 />
        <gridsize = 0,0 />
        <wincoor = 0,0 />
        <winstate = normal />
        <blinkrate = 400ms />
        <fontlist> <!-- Font fallback list (LF-delimited (\n), ordered). The rest of the fonts available in the system will be loaded dynamically. -->
            "Courier New\n" <!-- Primary font. Its metrics define the cell geometry. -->
            "Cascadia Mono\n"
            "Fira Code\n"
            "NSimSun\n"
            "Noto Sans Devanagari\n"
        </fontlist>
    </gui>
    <menu>
        <item id="Test" type=dtvt param="$0 -r test"/>
    </menu>
</config>
```

Use the `--gui` cli option to force it to run in GUI mode.

</details>
Wow. No wonder WinTerm is looking so good. You guys are incredible!

For comparison, here's some examples from whatever MS Office uses to render. You can see that they've just prerendered a simple squiggle and just roll with it. And what I'm seeing in Opera at the bottom.

Seems like you could come up with a good pre-render (or render on fly + cache) for a given integer # vertical pixels, and then apply the right one given the space available. I would like to see 1-pixel of separation, which is what Opera [or WebKit/whatever] is obviously doing, But even abutment looks decent, especially if the undercurl color is different than the font.

Consolas
6pt
![image](https://github.com/microsoft/terminal/assets/6503931/840c96d6-433c-4930-aae1-8af80fc626d2)

8pt
![image](https://github.com/microsoft/terminal/assets/6503931/d850418f-0ce3-4f72-94eb-1a79e5b7fe21)

9pt
![image](https://github.com/microsoft/terminal/assets/6503931/67f604dd-b108-48ce-a9d5-a69d708597f7)

20pt
![image](https://github.com/microsoft/terminal/assets/6503931/47f408b0-066d-4a8e-a4b2-71e442790dd1)

10pt, TextZoom=200%
![image](https://github.com/microsoft/terminal/assets/6503931/e58a47c8-d566-430d-8c17-59c787032e3d)

Here's the undercurl I see while typing this in Opera.
![image](https://github.com/microsoft/terminal/assets/6503931/ced1c700-7cf5-4fea-8a08-0c52b7431d68)


BTW, you can see that at 6pt, the Office curl does move into the bottom line of pixels of the font. Bug, IMO.
trying__underscore in Opera.
The 1-pixel separation helps prevent heavy collision with underscores.
![image](https://github.com/microsoft/terminal/assets/6503931/401b2c07-6470-4c06-ae05-28e96b12caba)


WinTerm nearly obliterates the underscore.
![image](https://github.com/microsoft/terminal/assets/6503931/abffe024-c34e-450d-b3c2-658f69963cca)

Bump-up font size.
![image](https://github.com/microsoft/terminal/assets/6503931/a2a35ab3-90f5-468a-89ce-9fbe2273f775)

Top is WezTerm, bottom is WinTerm.
I don't have WezTerm underline_position changed from default. I assume I could use it to move the curl down though
WezTerm appears to render the text on top of the curl. This makes it easy to discern _ chars, esp when coloring is working (I can get it to work in test prints, but not nvim; topic for different issue).
WinTerm is rendering curl on top to text.
I believe curl should go under text.
![image](https://github.com/microsoft/terminal/assets/6503931/7c07a278-e4f8-4194-bf23-dc4ee7db615e)


Fix patch:
diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp
index 9a83b75d233..a9e81422d0a 100644
--- a/src/renderer/atlas/BackendD3D.cpp
+++ b/src/renderer/atlas/BackendD3D.cpp
@@ -306,7 +306,7 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
         // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc.
         // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min().
         const auto height = std::max(3, duBottom + duHeight - duTop);
-        const auto position = std::min(duTop, cellHeight - height - duHeight);
+        const auto position = std::min(duTop, cellHeight - height);
 
         _curlyLineHalfHeight = height * 0.5f;
         _curlyUnderline.position = gsl::narrow_cast<u16>(position);
diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl
index d8d894be13e..f52a13f48eb 100644
--- a/src/renderer/atlas/shader_ps.hlsl
+++ b/src/renderer/atlas/shader_ps.hlsl
@@ -37,7 +37,7 @@ Output main(PSData data) : SV_Target
     {
     case SHADING_TYPE_TEXT_BACKGROUND:
     {
-        const float2 cell = data.position.xy / backgroundCellSize;
+        float2 cell = data.position.xy / backgroundCellSize;
         color = all(cell < backgroundCellCount) ? background[cell] : backgroundColor;
         weights = float4(1, 1, 1, 1);
         break;
@@ -45,13 +45,13 @@ Output main(PSData data) : SV_Target
     case SHADING_TYPE_TEXT_GRAYSCALE:
     {
         // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
-        const float4 foreground = premultiplyColor(data.color);
-        const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
-        const float intensity = DWrite_CalcColorIntensity(data.color.rgb);
+        float4 foreground = premultiplyColor(data.color);
+        float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
+        float intensity = DWrite_CalcColorIntensity(data.color.rgb);
         // These aren't.
-        const float4 glyph = glyphAtlas[data.texcoord];
-        const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
-        const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);
+        float4 glyph = glyphAtlas[data.texcoord];
+        float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
+        float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);
         color = alphaCorrected * foreground;
         weights = color.aaaa;
         break;
@@ -59,11 +59,11 @@ Output main(PSData data) : SV_Target
     case SHADING_TYPE_TEXT_CLEARTYPE:
     {
         // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
-        const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
+        float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
         // These aren't.
-        const float4 glyph = glyphAtlas[data.texcoord];
-        const float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast);
-        const float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios);
+        float4 glyph = glyphAtlas[data.texcoord];
+        float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast);
+        float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios);
         weights = float4(alphaCorrected * data.color.a, 1);
         color = weights * data.color;
         break;
@@ -157,26 +157,53 @@ Output main(PSData data) : SV_Target
     }
     case SHADING_TYPE_DOTTED_LINE:
     {
-        const bool on = frac(data.position.x / (3.0f * underlineWidth * data.renditionScale.x)) < (1.0f / 3.0f);
+        bool on = frac(data.position.x / (3.0f * underlineWidth * data.renditionScale.x)) < (1.0f / 3.0f);
         color = on * premultiplyColor(data.color);
         weights = color.aaaa;
         break;
     }
     case SHADING_TYPE_DASHED_LINE:
     {
-        const bool on = frac(data.position.x / (6.0f * underlineWidth * data.renditionScale.x)) < (4.0f / 6.0f);
+        bool on = frac(data.position.x / (6.0f * underlineWidth * data.renditionScale.x)) < (4.0f / 6.0f);
         color = on * premultiplyColor(data.color);
         weights = color.aaaa;
         break;
     }
     case SHADING_TYPE_CURLY_LINE:
     {
-        const float strokeWidthHalf = doubleUnderlineWidth * data.renditionScale.y * 0.5f;
-        const float amp = (curlyLineHalfHeight - strokeWidthHalf) * data.renditionScale.y;
-        const float freq = data.renditionScale.x / curlyLineHalfHeight * 1.57079632679489661923f;
-        const float s = sin(data.position.x * freq) * amp;
-        const float d = abs(curlyLineHalfHeight - data.texcoord.y - s);
-        const float a = 1 - saturate(d - strokeWidthHalf);
+        // The curly line has the same thickness as a double underline.
+        // We halve it to make the math a bit easier.
+        float strokeWidthHalf = doubleUnderlineWidth * data.renditionScale.y * 0.5f;
+        float amplitude = (curlyLineHalfHeight - strokeWidthHalf) * data.renditionScale.y;
+        // We multiply the frequency by pi/2 to get a sine wave which has an integer period.
+        // This makes every period of the wave look exactly the same.
+        float frequency = data.renditionScale.x / curlyLineHalfHeight * 1.57079632679489661923f;
+        // At very small sizes, like when the wave is just 3px tall and 1px wide, it'll look too fat and/or blurry.
+        // Because we multiplied our frequency with pi, the extrema of the curve and its intersections with the
+        // centerline always occur right between two pixels. This causes both to be lit with the same color.
+        // By adding a small phase shift, we can break this symmetry up. It'll make the wave look a lot more crispy.
+        float phase = 1.57079632679489661923f;
+        float sine = sin(data.position.x * frequency + phase);
+        // We use the distance to the sine curve as its alpha value - the closer the more opaque.
+        // To give it a smooth appearance we don't want to simply calculate the vertical distance to the curve:
+        //   abs(pixel.y - sin(pixel.x))
+        //
+        // ...because while a pixel may be vertically far away it may be horizontally close to the sine curve.
+        // We need a proper distance calculation. This makes a large difference at especially small font sizes.
+        //
+        // While calculating the distance to a sine curve is complex, calculating the distance to its tangent is easy,
+        // because tangents are straight lines and line-point distance are trivial. The tangent of sin(x) is cos(x).
+        // The line-point distance is the vertical distance multiplied by the cos(angle) of the line.
+        // To turn out tangent cos(x) into an angle we need to calculate atan(cos(x)). This nets us:
+        //   abs(pixel.y - sin(pixel.x)) * cos(atan(cos(pixel.x))
+        //
+        // The expanded sine form of cos(atan(cos(x))) is 1 / sqrt(2 - sin(x)^2), which results in:
+        //   abs(pixel.y - sin(pixel.x)) * rsqrt(2 - sin(pixel.x)^2)
+        float distance = abs(curlyLineHalfHeight - data.texcoord.y - sine * amplitude) * rsqrt(2 - sine * sine);
+        // Since pixel coordinates are always offset by half a pixel (i.e. data.texcoord is 1.5f, 2.5f, 3.5f, ...)
+        // the distance is also off by half a pixel. We undo that by adding half a pixel to the distance.
+        // This gives the line its proper thickness appearance.
+        float a = 1 - saturate(distance - strokeWidthHalf + 0.5f);
         color = a * premultiplyColor(data.color);
         weights = color.aaaa;
         break;


Test patch: