jtdaugherty / brick

A declarative Unix terminal UI library written in Haskell
Other
1.61k stars 164 forks source link

Text-like widget layout with wrapping #400

Closed xsebek closed 11 months ago

xsebek commented 2 years ago

I would like to have a layout for widgets that works like text:

textLayout [uuuu, vv, www, xxxxx, yyyyyy, z]  -- textLayout may not be the best name ofc...
|uuuuvvwww |
|xxxxx     |
|yyyyyyz   |

The idea is that txtWrap myText === textLayot (map txt) (words myText). This would allow me to highlight words in the text.

However, I imagine this could be useful even for widgets with more than one vertical line. For example for filesystem icons that are laid out in a grid and are overflowing to multiple lines:

+----------+
|+-++-++-+ |
||A||B||C| |
|+-++-++-+ |
|          |
|+-++-+    |
||i||k|    |
|+-++-+    |
+----------+

Is there some combination of functions in brick to do this already?

jtdaugherty commented 2 years ago

@xsebek Interesting idea! There isn't currently anything in the library that does this. But I think that if you make the constraining assumption (requirement) that all of the widgets to be laid out have Fixed size policies in both dimensions, it seems possible (even borrowing some of the logic from the text-wrapping implementation).

You'd also need to make decisions about how to deal with vertical positioning of widgets in the same "line" when their heights differ; it could get tricky depending on the use case that it's intended to help with.

For example, if you wanted to lay out these widgets and "wrap" them as needed,

ABC          D         FG
             E

in a space with only enough to show the first two on a line, you'd get

ABCD
   E
FG

but a question that comes to my mind is whether someone could want to control the vertical alignment behavior of widgets in the "line" that are shorter than the tallest one, possibly yielding a different layout,

   D
ABCE
FG

The user could just wrap the first widget in a padTop Max but that would violate the requirement that all widgets be Fixed in size policy. Anyway, just some thoughts I had as I tried to work out whether this could be done. I think it can, but what isn't clear to me is whether it'd be flexible enough for all use cases (whatever those are) and how API ergonomics would work out to expose such flexibility to the user. Naturally, the issue I've described here doesn't come up when all of the items to be wrapped are guaranteed to have exactly one row in height, as is the case with ordinary text wrapping.

jtdaugherty commented 2 years ago

I might have some time to try coding this up. Can you say a bit more about the use case you have in mind?

jtdaugherty commented 2 years ago

One other minor detail to point out, not that I think it matters in your case, but: txtWrap myText === textLayot (map txt) (words myText) is not quite possible just because whitespace is significant in between adjacent tokens in the txtWrap case, but no whitespace (at the logical level) is involved in the textLayout case. In that case the words would essentially be assumed to be separated pairwise by a single space each. I think I know what you mean well enough, but I realized this while thinking about whether something like textLayout could be generic enough to replace txtWrap!

xsebek commented 2 years ago

Thanks for looking into this @jtdaugherty and clarifying some of the cases. :+1:

My use case is something like a simplified Markdown - just highlight text surrounded in backticks (swarm-game/swarm#545). Another use case that I think would be highly useful is highlighting usernames or searched words. It should still be part of the text so hBox does not work.

I wonder if there could specifically be an implementation for one-line widgets (greedy horizontally can be put on its own line) that would take the textWrap logic mostly as is. Maybe it would be best to have a few functions like:

-- | Takes Widgets that have vertical growth policy fixed to 1 and lays them on the line with
--   widgets that do not fit or are greedy horizontally overflowing to the next line.
lineWrap :: [Widget n] -> Widget n

-- | Generalized version of 'lineWrap' that accepts Widgets of different fixed vertical sizes
--  and lays them next to each other horizontally with those that do not fit overflowing to the
--  next "line". 
hBoxWrap :: TopOrCenterOrBottom -> [Widget n] -> Widget n
jtdaugherty commented 2 years ago

The thing we need to keep in mind is that Widgets are going to be more expensive to render in the fully general case than individual text nodes will be, because at least with text nodes, you know enough about their structure that you can render them directly to images rather than firing up the relatively expensive widget-rendering machinery.

With that said, I think that if your main use case is to deal better with text nodes (for e.g. highlighting) then your best bet is to convert your text into an intermediate AST that can then be turned into a single rendering more efficiently. I've done that with Markdown in the matterhorn project, and it worked out very well there; we also had to do text-wrapping in the midst of other concerns (even such as breaking up hyperlinked word sequences over line endings).

So before going for a fully general implementation in the library like wrapStuff :: [Widget n] -> Widget n I'd suggest doing something like parseMarkdown :: Text -> Nodes and then writing wrapNodes :: Int -> Nodes -> [Nodes] and finally renderNodes :: Nodes -> Widget n. That's essentially what I did in Matterhorn, and it's served us very well in that context.

Incidentally, I've thought about how to take Matterhorn's Markdown rendering implementation and generalize it enough to be included in brick itself. I haven't done it yet because 1) Matterhorn uses CommonMark and not everyone will want to use that (although I would advocate for it) and 2) there are some other things about the Matterhorn implementation that make that difficult (it has a handful of custom node types specific to Mattermost chat messages that I'd need to find a nice way of generalizing out of that API). I'd still like to think more about it, though.

jtdaugherty commented 2 years ago

Another idea I just had is to generalize my word-wrap package so that it works on any stream of input that is tokenizable and thus wrappable. I'll think about that, too. It probably isn't a ton of work to pull out the text-specific bits of that and write a type class for tokenizable/wrappable stuff.

jtdaugherty commented 2 years ago

(That would take care of the wrapping step even in the presence of your own token type.)

xsebek commented 2 years ago

Ah, good point, I suppose it is necessary to do the line wrapping before turning text to widgets.

Thanks for explaining how matterhorn does it, if that logic was included in brick or a separate package I would be happy to use it and not reinvent the wheel. 😅

I think the general widget wrapping function would still be useful and it could have a note that for text it’s better to do it this way.

jtdaugherty commented 2 years ago

if that logic was included in brick or a separate package

I'll update here as I'm able to work on it.

frasertweedale commented 2 years ago

How we do it (highlight matched substrings) in Purebred: https://github.com/purebred-mua/purebred/blob/master/src/Purebred/Types/Presentation/MailBody.hs#L113-L147. It works line-by-line (substrings that wrap across lines will not be found).

We previously used brick's own markup implementation (which was removed a little while back). But it did not perform well and was more general than the simple highlighting we needed.

If we're talking about things like rendering markup, why not the Pandoc AST? We are quite likely to go down that path for customisable entity presentation in Purebred.

jtdaugherty commented 2 years ago

@frasertweedale I think the key concern here is wrapping, not markup rendering per se. The issue with Brick's current wrapping implementation is that it is not composable with anything else, which is problematic if the thing you want to wrap needs to contain extra information (like token types, attribute names, etc.).

What I'm currently noodling on is generalizing word-wrap to work on a sequence of tokens that can wrapped rather than just a Text that can be split into words. That would make it possible to tokenize your content in your own application-specific way, probably implement a type class instance from word-wrap, and then apply the wrapping logic there while still using your own representation. (And keeping the rendering step itself separate, as it should be.)

kostmo commented 1 year ago

I believe this layout style is popularly known as FlowLayout.

jtdaugherty commented 11 months ago

Doing a ticket review, I noticed this one and I'm afraid to say I'm not going to get to it any time soon. Thinking just in terms of flow layout for Widget, it's possible as long as the items in question have fixed sizes. Before adding anything like that to the core library, I'd want to prototype it and see how it'll work out in practice. If someone wants to take a stab at that, I'd be happy to offer advice. In the mean time, I'm going to close this as a thing that I am not realistically going to be able to get to. But if someone else wants to work on it, please re-open!