japhb / Terminal-Widgets

Simple Raku widgets for full-screen TUIs
Artistic License 2.0
4 stars 3 forks source link

Feature: GroupBox #11

Open patrickbkr opened 7 months ago

patrickbkr commented 7 months ago

This is a feature request. I am willing to work on this myself if it's agreed to be useful and we agree on how it should work and nobody beats me to it. As I haven't yet fully dug trough all of the T::W sources, it's well possible that the below is already implemented and I just didn't see it.

Here is a mockup of my current idea of how the debugger I'm working on should look:

_T_hread 1 > _F_rame 47 > My/ClassyClass.rakumod:7
- Source ----------------------------- Locals ---------------------------------
5 |class My::ClassyClass {          | > $foo (Str)  = iea
6 |    method do() {                | > $!bar (Int) = 5
7>|        say "Yay!";              | > @baz (List) = 1, 3, 5, 7, ...
8 |    }                            | ...
9 |}                                |
-------------------------------------------------------------------------------
| Source | Locals | REPL | Console | Breakpoints (7) | Help | Quit

Notice the Source and Locals words in the second line. Those are meant as a description of the boxes below.

This could either be modeled as boxes with a label. Bonus points if touching boxes share borders / manage to use up only one cell width when they touch. In many GUI Frameworks such boxes are called GroupBox.

A less full-featured approach could be to model this as a LabeledDivider. Then the above layout would have two columns each with a LabeledDivider first and the content second.

japhb commented 7 months ago

First: patrickbkr++ for filing a good feature request!

As it happens, writing on borders is on my list already, because as I was doing a bit of a survey for https://github.com/japhb/Terminal-Widgets/issues/8 I realized how very common that is. It hadn't been high priority previously, but this easily bumps it up. Lemme see what I can hack together, or if I can break a chunk of work out for you. :-)

japhb commented 7 months ago

OK, looking further at the mockup in this FR, I can see several things that T-W needs:

Anything else I missed there?

japhb commented 7 months ago

The above debugger-mockup commit gives us something to discuss. It's missing several things in the original request, but it's a start.

patrickbkr commented 7 months ago

Wow! That was again a very quick and thorough response!

I think WRT features we need to strike a balance. Not all of the listed features make sense to have a dedicated Widget implemented in T::W directly, because they are not generic enough or are simple to produce using the existing Widgets to not legitimize a special widget for the purpose. So sometimes the solution might be to extend the existing Widgets to allow easily composing them to reach the wanted outcome.

Especially Autonumbered buffer lines and Margin marks for buffers seem to me to be things that are ok-ish simple to do with two text boxes and a divider, given the text boxes provide enough introspection into their contents and enough control over the scroll position. Tab rows and Breadcrumbs seem to be easily done using Buttons. (I may fatally underestimate the task, what do you think?)

I don't yet have a full overview of the existing T::W feature set. So the following more extensive explanation of the debugger UI is a "what I want" not "how to achieve it":

The buttons at the bottom should always be visible. The rest of the screen changes dynamically.

The default view is made up of two rows. The first contains Source, Locals, Breakpoints, the second contains REPL and STDIO. Each of these can be hidden / shown. When hidden the remaining ones take the space. Then there are separate views that always take up the full screen when activated: Help, Thread, Frame, Protocol.

Thread and Frame are each a simple selection list that lets one pick a thread / frame. Help is some nicely formatted and layed out text, Protocol is a log of the debugger protocol messages going over the wire.

I'd like to dynamically label and stylize the buttons. STDIO should change when output arrives (maybe also have a count of new lines). All the buttons should change style depending on whether their box is visible or not.

Also I'd like shortcuts to work seamlessly. Each button should have one letter highlighted. I'm not entirely decided yet, but maybe a simple ALT + letter to activate the button.

I'm undecided on how to do it exactly, but I need a way to move the cursor from element to element (with a mouse that's obvious, but I'd like to provide a good keyboard-only user experience as well). Either I lean on a good "tab order" and allow tabbing through windows / buttons (that can get annoying, but is easy to grasp), or I introduce another modifier key and have e.g. CTRL + letter jump to the respective window (or both).

Independent of my debugger project, I think it's good for T::W to have excellent support for keyboard navigation. That means a robust way to specify tab order and an easy way to activate elements by shortcut.

japhb commented 7 months ago

Wow! That was again a very quick and thorough response!

I aim to please. :wink:

I think WRT features we need to strike a balance. Not all of the listed features make sense to have a dedicated Widget implemented in T::W directly, because they are not generic enough or are simple to produce using the existing Widgets to not legitimize a special widget for the purpose. So sometimes the solution might be to extend the existing Widgets to allow easily composing them to reach the wanted outcome.

Completely agreed. FWIW, I wasn't thinking that everything listed would be dedicated widget classes, rather that I needed to think of ways to make them possible and hopefully not painful to implement. Providing more potential than policy, in other words. T::W is currently somewhat opinionated, but a lot of that is because I needed to get something working, not because I wanted to make a super-opinionated toolkit.

Especially Autonumbered buffer lines and Margin marks for buffers seem to me to be things that are ok-ish simple to do with two text boxes and a divider, given the text boxes provide enough introspection into their contents and enough control over the scroll position.

Both of these are probably even easier to implement as just formatted spans at the start of every line (now that displayed lines can have arbitrarily styled spans throughout). That also cuts down on number of active widgets and need to keep things quite so much in sync. (Both of which should improve speed and maintainability a bit.) Edit: Done in 765daa9

Tab rows and Breadcrumbs seem to be easily done using Buttons. (I may fatally underestimate the task, what do you think?)

I was planning to have Tab Rows be pretty much a special styling of what underneath the covers is essentially a radio button group. Breadcrumbs could be done a couple different ways (and what you mentioned is one of those). Hadn't quite decided yet.

I don't yet have a full overview of the existing T::W feature set. So the following more extensive explanation of the debugger UI is a "what I want" not "how to achieve it":

Perfectly reasonable to me. Helps to avoid XY questions.

The buttons at the bottom should always be visible. The rest of the screen changes dynamically.

Does this include when the "full screen" choices are up, or do those have their own exit method (button, Escape key, whatever)?

The default view is made up of two rows. The first contains Source, Locals, Breakpoints, the second contains REPL and STDIO. Each of these can be hidden / shown. When hidden the remaining ones take the space. Then there are separate views that always take up the full screen when activated: Help, Thread, Frame, Protocol.

Oh interesting. This will be the first time I've needed to change geometry of individual widgets without resizing the entire terminal window. If I get that right I might be able to allow draggable borders to change relative sizes ... hmmm. Will have to think on this.

Thread and Frame are each a simple selection list that lets one pick a thread / frame.

These sounds like they might work as pop-up scrolling selection lists rather than full screen toplevels -- but there might also be value in representing these as tree views with collapse/expand semantics.

Help is some nicely formatted and layed out text, Protocol is a log of the debugger protocol messages going over the wire.

Those should both be relatively easy, though the first one certainly increases the task priority for inline markup. The latter is pretty much the ideal use case for the Viewer::Log widget.

I'd like to dynamically label and stylize the buttons. STDIO should change when output arrives (maybe also have a count of new lines). All the buttons should change style depending on whether their box is visible or not.

Ah, this is interesting from a changing-layout point of view. It also means I need stateful buttons (essentially checkbox semantics under the covers, but styled like buttons).

Also I'd like shortcuts to work seamlessly. Each button should have one letter highlighted. I'm not entirely decided yet, but maybe a simple ALT + letter to activate the button.

That should be possible to simulate by just catching the keyboard events when they bubble up to the toplevel ... having T::W just autodetect available keyboard shortcuts would be a tad more work, but might be useful for translations of the user interface (to use different letters in each language).

I'm undecided on how to do it exactly, but I need a way to move the cursor from element to element (with a mouse that's obvious, but I'd like to provide a good keyboard-only user experience as well). Either I lean on a good "tab order" and allow tabbing through windows / buttons (that can get annoying, but is easy to grasp), or I introduce another modifier key and have e.g. CTRL + letter jump to the respective window (or both).

Tab/Shift-Tab already works to navigate between Input subclasses, but it's based on a very general first/prev/next/last widget tree search mechanism, so that shouldn't be too hard to extend significantly.

Independent of my debugger project, I think it's good for T::W to have excellent support for keyboard navigation. That means a robust way to specify tab order and an easy way to activate elements by shortcut.

100% AGREED. Already part of the way there. :-)

japhb commented 7 months ago

Some new commits:

patrickbkr commented 7 months ago

Completely agreed. FWIW, I wasn't thinking that everything listed would be dedicated widget classes, rather that I needed to think of ways to make them possible and hopefully not painful to implement. Providing more potential than policy, in other words. T::W is currently somewhat opinionated, but a lot of that is because I needed to get something working, not because I wanted to make a super-opinionated toolkit.

Then we are on the same page. :-)

Especially Autonumbered buffer lines and Margin marks for buffers seem to me to be things that are ok-ish simple to do with two text boxes and a divider, given the text boxes provide enough introspection into their contents and enough control over the scroll position.

Both of these are probably even easier to implement as just formatted spans at the start of every line (now that displayed lines can have arbitrarily styled spans throughout). That also cuts down on number of active widgets and need to keep things quite so much in sync. (Both of which should improve speed and maintainability a bit.) Edit: Done in 765daa9

One (small) disadvantage of adding the line numbers directly to the text, is that I need to use up the full width of the line number even in the <100 and <1000 line range. If those are separate I could look at the largest number on screen and make the number column only that large. (It's more complex than that, because the width of the content box then also changes, potentially wrapping lines.) I think this is okay for files shorter than 1000 lines, but could be a bit annoying for a file that exceeds the 1000 lines.

The buttons at the bottom should always be visible. The rest of the screen changes dynamically.

Does this include when the "full screen" choices are up, or do those have their own exit method (button, Escape key, whatever)?

My intention was to have the bottom row always visible even with the "fullscreen" views.

Thread and Frame are each a simple selection list that lets one pick a thread / frame.

These sounds like they might work as pop-up scrolling selection lists rather than full screen toplevels -- but there might also be value in representing these as tree views with collapse/expand semantics.

The only reason I leaned towards a full screen view was that I want the Thread and Frame views to display thread name, file name, line number, maybe even a fragment of the source line or the name surrounding routine for each frame. That takes a lot of width. A tree view to allow expanding threads could be helpful, but I need to ponder use cases a bit to evaluate if separate views or expandable tree views are less annoying.

Help is some nicely formatted and layed out text, Protocol is a log of the debugger protocol messages going over the wire.

Those should both be relatively easy, though the first one certainly increases the task priority for inline markup. The latter is pretty much the ideal use case for the Viewer::Log widget.

I already have the Protocol view working. That was very easy to do using Viewer::Log. :-)

japhb commented 7 months ago

One (small) disadvantage of adding the line numbers directly to the text, is that I need to use up the full width of the line number even in the <100 and <1000 line range. If those are separate I could look at the largest number on screen and make the number column only that large. (It's more complex than that, because the width of the content box then also changes, potentially wrapping lines.) I think this is okay for files shorter than 1000 lines, but could be a bit annoying for a file that exceeds the 1000 lines.

No particular reason that you'd always need to use the widest possible line number space; the way that SpanBuffer works, it requests a line range at render time and you return formatted line spans. You can change that formatting each render frame to optimize visual space if you so desire.

There is a different problem with this technique though ... the line numbers will scroll off the left side when horizontally scrolling long lines. If this isn't what you want, you can either soft-wrap the lines (wrapping them just for rendering) and get rid of the horizontal scrollbar completely, or do what you initially suggested and put the line numbers and current line marker into a separate widget to the side of the actual source file buffer.

The buttons at the bottom should always be visible. The rest of the screen changes dynamically. All the buttons should change style depending on whether their box is visible or not.

OK, gotcha. I've done even more refactoring cleanup, and now have a simple ToggleButton class that looks like a button but acts like a checkbox (toggling and showing latest state each time it is clicked). Right now the actual appearance of these isn't the final version, but I need to spread around the span-formatting love a bit more first. (Right now Inputs can't handle a Span or SpanTree as their label, but that'll change very soon.)

The default view is made up of two rows. The first contains Source, Locals, Breakpoints, the second contains REPL and STDIO. Each of these can be hidden / shown. When hidden the remaining ones take the space. Then there are separate views that always take up the full screen when activated: Help, Thread, Frame, Protocol.

Button names changed (and button types changed to ToggleButtons instead of standard momentary Buttons), though they are still just a mockup and don't actually hide/show panes.

Does this include when the "full screen" choices are up, or do those have their own exit method (button, Escape key, whatever)?

My intention was to have the bottom row always visible even with the "fullscreen" views.

Gotcha. Right now it's sounding like you have a thin header (breadcrumbs and perhaps a few other small bits), a thin footer (the view control buttons and a global Quit button), and then a large content area in the middle that can be formatted as tiled panes filling a 3 x 2 tile area. One of those tiled layouts is just a single tile covering the entire middle content area, and those are what you seem to mean by "fullscreen". Am I close?

Thread and Frame are each a simple selection list that lets one pick a thread / frame.

These sounds like they might work as pop-up scrolling selection lists rather than full screen toplevels -- but there might also be value in representing these as tree views with collapse/expand semantics.

The only reason I leaned towards a full screen view was that I want the Thread and Frame views to display thread name, file name, line number, maybe even a fragment of the source line or the name surrounding routine for each frame. That takes a lot of width. A tree view to allow expanding threads could be helpful, but I need to ponder use cases a bit to evaluate if separate views or expandable tree views are less annoying.

Ah yeah, gotcha.

japhb commented 7 months ago

I think I might have figured out a path forward for doing pane show/hide without messing with the overall structure, using a rarely-used feature of the layout constraint solver. However, it is the wee hours of the morning here, so I need to take a look at this again with a fresh brain.

Here's the idea so far:

There is one (possibly big) remaining problem to solve: getting rid of the scrollbars when the share of the parent layout node gets too small, so that they don't "hold the parent layout open" just by requiring a minimum size to exist.

patrickbkr commented 7 months ago

My intention was to have the bottom row always visible even with the "fullscreen" views.

Gotcha. Right now it's sounding like you have a thin header (breadcrumbs and perhaps a few other small bits), a thin footer (the view control buttons and a global Quit button), and then a large content area in the middle that can be formatted as tiled panes filling a 3 x 2 tile area. One of those tiled layouts is just a single tile covering the entire middle content area, and those are what you seem to mean by "fullscreen". Am I close?

Yes, that's pretty much what I envisioned. I'm pretty sure my ideas will fail the test of reality here and there, but yes, that's my current idea. (Not entirely sure, maybe I want the top row to disappear in the other "fullscreen" views.)

patrickbkr commented 7 months ago

I think I might have figured out a path forward for doing pane show/hide without messing with the overall structure, using a rarely-used feature of the layout constraint solver. However, it is the wee hours of the morning here, so I need to take a look at this again with a fresh brain.

Here's the idea so far:

* Keep track of desired relative tile heights and width shares, plus the current state of each

* To hide, set current height and/or width share for a row or tile to zero and trigger a relayout

* To show, set share back to previous and trigger relayout again

* To allow moveable borders (and thus resizeable tiles):

  * As the border is dragged, set the shares on either side of that border to the exact fraction of actual height that the user has dragged to.
  * Exact rational numbers for the win!

There is one (possibly big) remaining problem to solve: getting rid of the scrollbars when the share of the parent layout node gets too small, so that they don't "hold the parent layout open" just by requiring a minimum size to exist.

Just curious: Intuitively my first idea went into the direction of either removing the hidden panes from their parent and then doing a relayout or giving panes a hidden attribute and in the layouter ignore such panes. I don't yet have a good understanding of the code, so: Is this a bad idea, and if so, why?

patrickbkr commented 7 months ago

Question: What's the idea behind the Form class? At the moment it seems to be populated in the examples but not used anywhere?

patrickbkr commented 7 months ago

And lastly an info: In the next days or weeks I'll probably have a lot less time to "keep up" with working on the debugger. My second child is arriving Really Soon (TM). So please don't be discouraged by my lesser involvement. I'm really impressed by how things move forward here. Big thanks to you! I do plan to be back. :-)

japhb commented 7 months ago

Yes, that's pretty much what I envisioned. I'm pretty sure my ideas will fail the test of reality here and there, but yes, that's my current idea. (Not entirely sure, maybe I want the top row to disappear in the other "fullscreen" views.)

Ah gotcha, that's not hard but does need to be considered in the overall layout.

Just curious: Intuitively my first idea went into the direction of either removing the hidden panes from their parent and then doing a relayout or giving panes a hidden attribute and in the layouter ignore such panes. I don't yet have a good understanding of the code, so: Is this a bad idea, and if so, why?

Actually, having a hidden attribute that tells the layout solver "assume the total size of this branch of the layout tree is 0x0" might actually be the best DevEx, because there's less work for the app developer (you, in this case), and less state to carry around in various spots. There's some work to be done to make sure all the rendering code handles collapsed/hidden widgets properly, but that should probably be done anyway.

It's not quite as general (doesn't handle the moving borders use case), but I'm fine with making the common case obvious, and leaving special cases to the escape hatch. Who knows, maybe I'll come up with a clean moving-border solution at some point also. :-)

Question: What's the idea behind the Form class? At the moment it seems to be populated in the examples but not used anywhere?

It ties together all the inputs that together constitute a single form, allowing you to programmatically introspect without having to copy and paste lists of input IDs. See for example https://github.com/japhb/Terminal-Widgets/blob/main/examples/form.raku#L51 . Beyond that, it allows you to have multiple forms visible at once (which is why the Form functionality isn't just folded into TopLevel), and in the future to allow submitting or reverting changes automatically by just adding submit/revert buttons to the form.

A weaker version of this is used for grouped form elements, such as radio buttons; each declares a group, and then you can easily and efficiently find all the radio buttons in the group given any single one of them. This is how selecting any radio button automatically deselects the others in the group: https://github.com/japhb/Terminal-Widgets/blob/main/lib/Terminal/Widgets/Input/Boolean.rakumod#L53-L59 .

And lastly an info: My second child is arriving Really Soon (TM).

CONGRATULATIONS! :tada: :balloon: :birthday:

In the next days or weeks I'll probably have a lot less time to "keep up" with working on the debugger. So please don't be discouraged by my lesser involvement. I'm really impressed by how things move forward here. Big thanks to you! I do plan to be back. :-)

No worries! Let me know when you have time and want some help. :-)