jtdaugherty / brick

A declarative Unix terminal UI library written in Haskell
Other
1.6k stars 163 forks source link

Mock render Widgets #405

Open xsebek opened 1 year ago

xsebek commented 1 year ago

I would like to be able to render widgets to Text.

The documentation of renderWidget suggests using the Vty library to render Picture, but it is not obvious to me how to do that.

What I would like is a function like this:

mockRenderWidget :: [Widget n] -> DisplayRegion -> Text

For example in the Swarm game we have recipes like this:

     3D printer
glass ──┬┴─── solar 
copper ─┘     panel
wires

Connecting widgets by lines looks really nice, but I would also like to print this to the stdout and save it on Wiki so that I don't have to run the game to see the recipes.


Another use case would be to doctest the UI functions. Something like:

-- | Render recipes.
-- >>> pustStrLn $ mockRenderWidget [renderRecipe solarPanelRecipe] (4,35)
--      3D printer
-- glass ──┬┴─── solar 
-- copper ─┘     panel
-- wires
renderRecipe :: Recipe -> Widget n

@jtdaugherty would you be interested in having such function in brick? :slightly_smiling_face:

Or maybe this is really simple and could just be added as a code snippet to the documentation.

jtdaugherty commented 1 year ago

@xsebek Right now there is something kinda close to this in the library: Brick.Main.renderWidget (docs here). It was added somewhat recently by someone who wanted to be able to write tests. It's more involved than what you're asking for here, though, because the Brick renderer needs to know some things in order to run. The function I linked to does not require an attribute map, but it does require a display region size (since that governs much of how UI rendering is done). It also outputs a Picture, which is of course much richer than just plain text.

One thing I want to highlight about your use case is that it assumes that the only things you'll be testing are things that have Fixed horizontal and vertical size policies. That covers a lot of things, but I wanted to make sure to point that out (since any documentation for any newly-added functionality would need to also document that assumption and the function would also need to have a good way of dealing with -- rejecting? -- widgets that declare infinite size).

Given the function in the library today,

renderWidget :: (Ord n) => Maybe AttrMap -> [Widget  n] -> DisplayRegion -> Picture

I think the task would be to write a wrapper, something like this:

renderWidgetPlain :: (Ord n) => Widget n -> Text
renderWidgetPlain w = pictureToText $ renderWidget Nothing [w] (1000, 1000)

pictureToText :: Picture -> Text
pictureToText = ???

It seems to me that pictureToText is the thing that would need to be written (and the assumptions encoded in the body of renderWidgetPlain would need to be documented emphatically as being only for testing purposes).

I don't have much time right now to look into what that would entail, but I'd be happy to review a patch if you or someone else wants to take a shot at it. I think pictureToText would go in the vty package if that happens. If someone does undertake this, I would strongly recommend doing it in a way that re-uses the Vty logic for image rendering. That could be tricky because Vty emits bytes, not Text, and escape sequences of various kinds would need to be either scrubbed (not my preference) or just omitted entirely at rendering time (what I'd hope we could do instead). I believe Vty has a mock output backend for testing that it uses in its test suite (that I didn't write and am not super familiar with). That might actually be the best place to start looking since perhaps it gets very close to what renderWidgetPlain will ultimately want anyway.

xsebek commented 1 year ago

Thanks for the detailed response and tips, @jtdaugherty! 👍 I am glad to hear I was not missing some obvious way to do this. 😅

I don't think I have encountered widgets with infinite size requirements, but I assume there has to be a way to wrap them and display them in a finite region which is what the mock function would do. 🙂

I'll take a look at the vty source and tests and see how hard this would be.

jtdaugherty commented 1 year ago

@xsebek I took a quick look and it looks like the implementation in Graphics.Vty.Output.Mock is close to what would be needed; it just needs to be duplicated and adjusted in a new backend that doesn't emit the metacharacters that the current mock backend emits. At that point renderWidgetPlain would need to be put into IO so that it can initialize Vty with the desired backend and then read from the place the backend writes (such as the IORef approach taken by the current mock backend).

jtdaugherty commented 1 year ago

@xsebek is this something you are still interested in working on? I'd like to close this ticket if this is dormant for now, and re-open it or open new ones later if you get into the work and have questions. Let me know!

avh4 commented 1 year ago

Just noting that I'm actively interested in this as well. I'm going to play around with Graphics.Vty.Output.Mock and see how it works for tests with the extra metacharacters, but if that doesn't do what I need very well, I'll likely make an attempt at implementing a pictureToText in the next few months if no one else has done it by then.

xsebek commented 1 year ago

@avh4 thanks for taking a look at this 👍

I wonder if we could hack around this, by using virtual terminal like screen which has (some?) a support for capturing output. Maybe this setup and capture could even be done from Haskell code. 🤔