Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spaces are not underlined when using the underline tag or an underlined font with FormattedText #38

Open
nhmkdev opened this issue Apr 9, 2017 · 4 comments
Assignees
Labels

Comments

@nhmkdev
Copy link
Owner

nhmkdev commented Apr 9, 2017

This would be nice to have fixed...

Note: Monospaced fonts will render underlines for trailing spaces in a string. Non-monospaced will not.

@nhmkdev nhmkdev added the bug label Apr 9, 2017
@nhmkdev nhmkdev self-assigned this Apr 9, 2017
@MarkVabulas
Copy link
Contributor

In the past, when implementing this functionality myself in my software, I've resorted to a special pass for underline, overline, and strike-through, which I've seen in other places like the Cairo formatter. Basically you calculate the contiguous size of the words, as a group, assuming the formatting isn't changing, and then draw a single horizontal line at the correct height and correct color. It can and should be assumed that you can have a horizontal word-break at any place where the format changes. For example, having Title<u> </u>Example should be able to give you an underline on just the space, in this method, and situations where you have Title are solved automatically since the format change is after the space.

One reason I think that the gradient was breaking for formatted text, but not for regular text, is that you're parsing the format of each word individually. A lot of other layout engines will grab the whole block which is identical, calculate in advance the layout of each word, including newlines, and then lay it out like that, keeping track of the current line and the current advance. This means it'll inform the next block of formatting where to begin, and it'll automatically pick up where it left off. One side-benefit of this method is that it provides for automatic line-height/line-spacing for formatted text, since the word's height is included in the new-line when doing the wrapping.

This keeping the rendering as per-block instead of per-word would also mean that you can pass any other shapes to the text-layout engine at the same time, and it can calculate intersections with complex shapes as it goes through each of the lines. Once each line is broken up at the correct spot, the layout is pushed to screen, horizontal strike lines are added, and you're done. And since you have text brushes, you can easily incorporate gradients back in as a side-effect.

As for the data structures, I have traditionally started with a raw string, and then broken it up into sections of identical formatting. Luckily, all of your formatting tags show you where the breaks are, so you can split that part up as a pre-pass, then create your format info to tag with each block. Also, for simplicity, I've often included any forced newlines as fixed \n at this point so that I make sure to incorporate them at the next step. Then block by block, you check the x and y advances with the next token's approximate size and see if it would exceed the bounds, if so, newline based on the current format's line height, then add the token to the next "line". This means you'll end up with a vector of strings, each entry into the vector is a uniform format. At the end of each string, you keep the newline to help the flow renderer. You should also make sure to never have a newline in the middle of an entry to this vector. Each vector entry is it's own line or a subpart of a single line which is too short or has multiple formats.

Once you've got all of those individual strings, calculating the width of each one to do overline/underline/strike-through is trivial. When it comes time to render, you just iterate your previous vector and pass the whole block as necessary with the identical formatting parameters. If the format is the same as the previous line, you can keep the previous parameters in this next iteration, for things like gradients etc. If you encounter a newline at the end of one of these vector entries, then go to the next line. If you don't encounter a newline, you know that the format has changed and should continue at the new advance position. Luckily, you've already calculated that for things like the horizontal lines, so you know where to begin again.

Yes, sadly, text flow layout is a 2-step process. I tried in vain for a long time to make it single pass. This process does have the added benefit that it lends itself well to using the GPU to do the rendering, since instead of using a blitting-pass/GDI, you can use it to make the GPU primitives necessary for repeated rendering.

@MarkVabulas
Copy link
Contributor

One side benefit I forgot to mention: the added benefit of pre-computing the positioning of blocks of text means align-bottom gets to be much easier for formatted text, so you can have parts aligned to the top and a certain section also aligned to the bottom of the available area.

@nhmkdev
Copy link
Owner Author

nhmkdev commented Apr 3, 2024

Thank you for all the thoughts and suggestions. 🧐

Admittedly the underlined issue has always annoyed me (just a bit) but so few people have ever complained about it. Some random thoughts:

  • I probably would introduce a whole new element type as FormattedText has a very layered and messy history. If I could dedicate a ton of time I would actually re-write many aspects of the application and make CardMaker2.
  • GDI does not help with these sorts of situations: <u>some text with a <b>bold</b>word</u> requiring manual logic (and likely terrible results).

On the gradients in FormattedText it's actually just an issue with the way rendering is done with translation transforms. I made a couple minor changes and was able to produce this:

image

I'll check into the rest of the required tweaks to see if there are any significant problems.

@MarkVabulas
Copy link
Contributor

From my experience, I always thought it would be difficult to manage the changing states, but in your example above of the nested changes, that would just end up being 3 different entries in the format/textblock vector. The first entry would be <u>some text with a </u> and no newline, and the second entry would be <u><b>bold</b></u> and the last entry would be <u>word</u>. Basically, anytime any of the formatting changes, you would make a new entry in your vector, copy the previous state, and make the necessary change. Keep the running tally of current states, and it can be single-pass to make your vector of blocks. I've often assumed this would take a lot to implement as well, but the last time I did the bulk of the work, including inline links that were clickable, it ended up being less than 100 lines of C++. (Inline links are another bonus, which you don't really need, of knowing before the second pass the regions of text as they're being rendered.) I know that your rendering routines are much more verbose in terms of what some of it can do, however it's actually a lot more straightforward when you actually get into it. Things like centering: easy since you can know the entire line's length/height after the first pass. Left/right/top/bottom alignment: easy, for the same reason. Word Spacing and Line Spacing are parameters which start as part of your global format but can be parameters for your blocks, if you desire. Bold/italic/underline/overline/strike-through are all pretty simple and assumed. Auto-Scale becomes much more intuitive because you find your biggest horizontal block and tallest height and just scale them all. That right there seems to cover all of your parameters in the software at the moment.

My suggestion would be in agreement with your suggestion to make a third type and eventually deprecate the other two, but the basis for the implementation could start off what you've already got working for basic Text, rather than FormattedText. After the incept parser runs, just gotta have a new one for the final output to turn into the text blocks, calculate the block sizes in real-time as you traverse it based on the current format state, and then drop that result into the GDI outputter. In some ways, the first pass can be done token-by-token, read if it's basic text, or an markup tag start/end, and act accordingly.

Btw, bravo with the formatted text gradients!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants