xtermjs / xterm.js

A terminal for the web
https://xtermjs.org/
MIT License
17.62k stars 1.63k forks source link

Curly and colored underlines #1145

Closed egmontkob closed 2 years ago

egmontkob commented 6 years ago

This is originally a feature of Kitty, now also adopted by VTE (GNOME Terminal and friends). Technically two separate features, but they mostly make sense together, e.g. for spell checking.

Apparently vim and neovim have already / are about to support these, see e.g. vim undercurl, vim color, neovim.


The new SGR 4:3 (\e[4:3m) attribute, strictly with a colon as separator, was introduced to start a curly underline.

In the mean time, 4:0, 4:1 and 4:2 were also added as aliases for the standard 24 (turn off all kinds of underlining), 4 (single underline) and 21 (double underline), respectively.

At some point in the future, probably 4:4 and 4:5 could also stand for dotted and dashed underlines in some order (these are the five types of underlining supported by HTML/CSS).


The new SGR 58 and 59 sequences specify the color of the underline, following the pattern of 38 and 39. That is, 58;5;idx for an entry of the 256-color palette, or 58;2;r;g;b for direct RGB. There's no shortcut notation for the first 16 entries (corresponding to SGR 30-37 and 90-97), use the 256-color mode with indices of 0-15 instead.

59 reverts to the default, that is, the underline's color auto-following the text color.

In case you're short of bits, I believe it's okay to drop some precision, e.g. store only 4 bits per color channel. We were also considering this in the VTE bug.

Update: In VTE we ended up approximating the truecolor underline to 4+5+4 bits (R, G, B respectively). This means: 8+8+8+1 = 25 bits for foreground (in addition to truecolor, we have to be able to denote palette and default values too). Same for background. 4+5+4+1 = 14 bits for underline (again, an extra bit to denote palette and default values). That's a total of 64 bits for the colors, stored in an int64 (bold, italic etc. are additional bits are next to this in another variable). With multiplication instead of bitpacking, it could have been 5+5+5 bits for the underline. Of couse it's not a necessity to put all the color and no other attributes in the same integer, we just decided it's nice this way (and results in some performance improvement).

egmontkob commented 6 years ago

(As per https://github.com/kovidgoyal/kitty/issues/226#issuecomment-381665023, mintty decided that 4:4 is dotted underline, and 4:5 is dashed.)

Tyriar commented 5 years ago

If someone wants to try put a PR together for this you can search the repo for "underline" to give you an idea of what has to happen. Basically support the escape sequence, enable storing it in the buffer and then support drawing it using the renderers.

jwhitley commented 4 years ago

As an aside, I note that both neovim and tmux now fully support undercurl in current releases as of this writing. It's pretty great when used with coc.nvim for LSP support, plus the appropriate undercurl highlight definitions.

jerch commented 4 years ago

We should implement this, its very useful for editors, debuggers etc. :smile_cat: Any takers?

There are 3 gems hidden here though:

egmontkob commented 4 years ago

the support for this is mostly non-existent

Really? Even two years ago, when I played with it, Firefox and Chromium supported curly and colored underlines.

(Chromium's one is much uglier, though. It can only draw entire periods, the last, partial period of the curly underline is completely omitted. It also stops and restart the curly underline on every DOM element boundary.)

Maybe even for browsers you could resort to some background image-like drawing? :)

Re bits: I've updated the original post.

jerch commented 4 years ago

@egmontkob

Really? Even two years ago, when I played with it, Firefox and Chromium supported curly and colored underlines.

Yeah, well its a mixed picture. I tested it with dashed/dotted, which leads to really ugly output or no underlines at all. Support for curly might be better. This needs some testing beforehand, still my guess is that we will have to resort to "drawing-by-hand" here to avoid box borders artefacts and such.

Re bits: I've updated the original post.

Thx for clarification. Yeah the attribute storage is an issue of its own, I tested several different approaches during the buffer rework. My favorite approach, storing them in a separate RB tree, led to lousy performance due to the needed additional tree lookup, while storing them in continous memory in the cells array itself is much much faster. Memory sacrifice for performance, as always. But with the underlines I wonder, if we should go that path again for a simple reason - underlines are very rare, thus the memory is wasted most of the time. Therefore a bit-notion in the cell and the additional lookup into a foreign storage might be sufficient here.

Edit: I wonder if the reduced underline palettes in VTE will create you color matching issues later on - the colors will slightly differ due to the reduced resolution, thus ppl cannot use FG/BG coloring tricks to temporarily hide those lines. Unlikely and rather an edge case, I know, but still a valid assumption from the sequence definitions.

jwhitley commented 4 years ago

FYI, there's a Chromium bug related to fixing the really poor rendering of text-decoration: underline wavy. That was blocked by the advent of "LayoutNG", which has apparently now landed.

So theoretically, it might be possible to avoid a custom renderer (for Chrome*, at least) if someone could be prodded into fixing that issue.

A cursory check via MDN's text-decoration page in FF vs Chrome shows that Firefox's wavy underline rendering is way better than Chromium's. Does FF also exhibit box boundary problems?

jwhitley commented 4 years ago

So theoretically, it might be possible to avoid a custom renderer (for Chrome*, at least) if someone could be prodded into fixing that issue.

Clarification: there's a good argument based on use cases like this one that wavy underline should render the wave angle based on a notion of absolute/viewport coordinates(?), not box-relative coordinates. If FF "does the right thing" on that point, it's additional ammunition to make that argument re: the Chromium issue.

egmontkob commented 4 years ago

[jerch] VTE ... ppl cannot use FG/BG coloring tricks to temporarily hide those lines

Whoever uses underline color = bg color to hide the underline, rather than disabling it: screw them! :)

(I mean: you have a valid point, we haven't thought of it. I guess we'll extend to 8+8+8 bits if it really becomes an issue.)

This leads us to the next question: if the underline crosses the letter, whose color should matter? VTE draws the underline fist, followed by the letter, so that the letter's color wins. That is, an underline color that equals to the background is a no-op (except for the color reduction). If we picked the other drawing order, it would be different. Followup question: what to do with skip-ink? It would not only be a PITA for us to implement, but I don't even like the end result, I prefer not to skip. :) All up to you to decide, I guess.

[jwhitley] LayoutNG ... So theoretically, it might be possible to avoid a custom renderer (for Chrome*, at least) if someone could be prodded into fixing that issue.

The announcement article you linked has a section "Joining across element boundaries", what it demonstrates seems to require pretty much the same architecture as the one needed for continuous undercurl. Let's hope they'll address it soon.

Another possibility could be if CSS had a text-decoration-underline-wavelength, or Chrome chose the font width (at least for monospace fonts), in that case the current problems its implementation has would not be visible with monospace fonts. Of course these are unlikely to happen.

jerch commented 4 years ago

Whoever uses underline color = bg color to hide the underline, rather than disabling it: screw them! :)

Haha yepp, still ppl gonna do funky things and think it was a smart move ;)

This leads us to the next question: if the underline crosses the letter ... Followup question: what to do with skip-ink? ...

Wasnt even aware of the skip-ink capability. Well both questions dive deep into typesetting realms, which I have no educated opinion about - can only tell you if something pleases my eyes lol. How would a typesetter put a colored, not "skip-inked" underline here, above or below? I cannot really answer that, in hand-written documents an underline would end up above most of the time (ppl would write the text first and underline afterwards, not the way around, but this is more a technical restriction not knowing the runwidth beforehand). Which gives the impression that the underline has a stronger meaning (strikes through letters), where an underline below is a weaker metaphor (letters strike through the line). Skip-ink looks indeed nice, I think ppl would use that mostly for the looks in the hand-written case (strike-through always looks intrusive). Thus the stronger vs. weaker metaphor might not be intended at all.

Transferred to the static nature of a terminal - I think below is the better way to handle it (if skip-ink is no option) to not disturb the letter flow to much. Skip-ink seems to be the holy grail here (for my eyes at least), but since we are used to underlines touching letters (+20ys staring at word documents or hyperlinks in browsers), not a big deal. Not sure how you implemented the glyph output in VTE, maybe a matrix transformation adding a hard glow/shadow could be used as erase mask? In general I think below is just fine. If we care enough we might look into skip-ink. Just abit scared that it might open the next can of worms.

About the buffer needs: I think we should put this information into a separate storage, something like ExtendedAttributes, that lives on a single buffer line. I see two main advantages here compared to directly extending the cell array:

Only downsides I see are the higher implementation needs (all cell actions must respect it) and higher memory and runtime costs for a cell, that holds those attributes (rare case). Gonna try to do a PR addressing the core changes first.

egmontkob commented 4 years ago

Transferred to the static nature of a terminal - I think below is the better way to handle it (if skip-ink is no option) to not disturb the letter flow to much.

I agree. The question reminds me of the "end of town" road signs in my country where usually the diagonal strikethrough is in the front, sometimes making it non-obvious what the exact name of the town is.

Not sure how you implemented the glyph output in VTE, maybe a matrix transformation adding a hard glow/shadow could be used as erase mask?

We don't have skip-ink in VTE, we just draw the underline first, followed by the letter. But if we were to implement skip-ink, we'd probably print the underline, then a boldified version of the letter with the background color (or the letter a couple of times, at small offsets in different directions from the desired position, which I think is effectively the same as your matrix idea), then finally the letter as desired.

If we care enough we might look into skip-ink. Just abit scared that it might open the next can of worms.

Like, for example: especially if the underline is wider than 1px and the underline crosses the letter at a sharp angle, the erase mask approach results in pointy underline ends. An even nicer looking result could imitate hand-drawing and lifting up the pen, by making the end always a rounded semi-circle. Probably extremely hard to implement; needs to be done inside the font renderer, or with the rendered variant at a higher resolution. And might not make too much sense, unless the letters in the font also imitate hand drawing. Let's quickly close this can of worms :)

jerch commented 4 years ago

An even nicer looking result could imitate hand-drawing and lifting up the pen, by making the end always a rounded semi-circle.

How about flourishes at the beginning and the end? We also have a strong need for medieval majuscles in the terminal. :rofl:

egmontkob commented 4 years ago

Definitely! :wink:

jerch commented 4 years ago

Made a quick hack on the DOM renderer with #2751 to get a first impression. Well, thats what I get in Chrome:

In comparison to FF:

Urgh, thats really bad for both. While FF still manages to output something useful (only shows box border artefacts) chrome is totally off. It cannot render curly/dashed/dotted reliably depending on the selected font size. A nightmare...

The CSS classes use text-decoration in the shorthand notation text-decoration: underline wavy|dotted|dashed|double;.

So thats where we start from with the DOM renderer, not quite promising...

jwhitley commented 4 years ago

So thats where we start from with the DOM renderer, not quite promising...

I was a bit afraid of that from rather less in-depth testing, just faffing with the MDN text-decoration page and FF/Chrome inspectors. At least at larger sizes, FF seemed much better than Chrome for underline wavy. Chrome is just.. questionable as heck.

jerch commented 4 years ago

@jwhitley What Chrome does is def. too unreliable, not showing the wavy line for certain font sizes is a full showstopper, the artefacts in dashed and dotted are also bad. FF does alot better, still the box border issues would lead to very poor output and should not be used. I think FF would do alot better if we would not put every char into its own box, but if we change that in the DOM renderer, we will re-introduce old grid-alignment bugs. To me it seems we cannot get this working with CSS styles. But before resorting to self-drawing - feel free to mess around with the DOM renderer and PR #2751, maybe you have a better idea how to get away cheaper.

jerch commented 4 years ago

Early self-drawing approach with DOM renderer:

grafik

The underline is actually a 3px high background image on the line, thus text will print on top. It seems easier to get complex underlines working with this approach, will see.

Tyriar commented 4 years ago

The underline is actually a 3px high background image on the line, thus text will print on top.

@jerch maybe we should shift the underline down a little if the user has lineHeight > 1?

jerch commented 4 years ago

@Tyriar Yes looks like. Well I have currently no time to work on this, so things have to wait. Ofc if anyone wants to step in, that would be great. Still gonna add myself to this issue to not lose track.

Tyriar commented 2 years ago

Fixed kitty docs link: https://github.com/kovidgoyal/kitty/blob/master/docs/underlines.rst

Tyriar commented 2 years ago

Test echo:

echo -e '\x1b[4:0m4:0 none\x1b[0m \x1b[4:1m4:1 straight\x1b[0m \x1b[4:2m4:2 double\x1b[0m \x1b[4:3m4:3 curly\x1b[0m \x1b[4:4m4:4 dotted\x1b[0m \x1b[4:5m4:5 dashed\x1b[0m'

Kitty v0.24.1 rendering:

Screen Shot 2022-07-23 at 6 57 17 am

xterm.js current state:

DOM:

Screen Shot 2022-07-23 at 6 58 46 am

Canvas:

Screen Shot 2022-07-23 at 6 59 35 am

Webgl:

Screen Shot 2022-07-23 at 6 59 49 am
Tyriar commented 2 years ago

Test echo for colors:

echo -e '\x1b[4;58:5:203mred underline (256)\x1b[0m \x1b[4;58:2:0:255:0:0mred underline (true color)\x1b[0m'