TUI Rant
Preface
I have no experience in user interface design whatsoever. Frankly, I should be reading some materials on how its problems are typically solved, but that's boring! Instead I'm going to come up with some ideas of my own, then see how they do after implementing them in a project.
For this project, I'm going with an interactive TUI (Textual User Interface) for use in terminal emulators, because I find them fun and they're easier to implement than non-textual equivalents.
Scaling
What I do know about designing an interface is that scaling is the hardest part. Here's what we need to consider in relation to scaling:
- Terminal transforms (position / scale) are measured in cells (character size) * Terminal emulators almost always use fixed-width fonts * There is always a minimum size requirement for a given interface * Parent window may be larger than you expect * Some elements may be more important than others * Misalignment of elements is to be avoided * Text width within elements should be considered * Scaling should be relatively smooth if possible * Focus directions should not change on resize
Okay, that's actually a lot. Let's go over them one by one.
Cells
This one actually makes implementation much easier, as we don't need to worry about converting elements' sizes when working with transforms. Okay, next item.
Fixed-Width Font (Monospace)
This is also a convenience, but it does come with a catch: characters are taller than they are wide, so we need to be wary of how that may distort dimensions when trying to draw things which are equally wide and tall. It's not a huge deal though.
Minimum Size
To clarify, I'm not referring to any technical limitation here. Rather, I mean that- with a small enough window- you will be unable to render anything useful. The easiest way to deal with this seems to be replacing the entire render with some text telling you the size requirements, as does btop.
Excess Size
While minimum size is pretty easy to test, it can be harder to tell when a greater width or height would cause unintended, unsightly scaling. This is especially problematic for me, as I use a single 1920x1080 monitor and that means I often have half or a quarter of my screenspace dedicated to testing. I can also move the test to a different workspace (bspwm <3), but that doesn't change the fact that others may be using 2k, 4k, or other monitors. Anyway, the simplest remedy for this is just to choose certain elements to scale up even when their contents don't need the room. Typically this works best when applied to the primary element: i.e. not a sidebar, toolbar, or similar.
Priority
Some non-essential elements may be okay to hide when the window is too small, and some should be forced to full size no matter what. Width and height might need to be independent for this; a column's list of elements can be shortened safely, but reducing the width of items makes them partially unreadable, and wrapping makes the list confusing to read.
Alignment
Say you have a primary panel and a sidebar. You likely want those two to have the same height regardless of window size. That's pretty easy to handle, but what if you had two primary panels stacked vertically and wanted the sidebar to be as tall as both panels combined? There are a couple ways to do this. One is to partition space with percentages, like so (using pseudocode):
layout { sidebar { width 0.2, height 1.0 }, panels { width 0.8, height 1.0, bottom { width 1.0, height 0.5 }, top { width 1.0, height 0.5 } } }
In this example, each element has a width and a height which are portions of its parent's width and height. This has the benefit of ensuring correct groupings and alignment, but it's also pretty verbose and makes the actual coordinates harder to follow. I present an alternative:
Pins
They're really more like guiding lines, but I like pins better. Anyway, the idea here is to define some invisible horizontal and vertical lines for element borders to snap to. The difference between this and the partitioning method is that pins would be ordered instead of portioned; the state and priority flags of every element are stored, then traversed by priority > pin order until the entire layout can be resolved or a pause state (e.g. window too small) reached. Having just written this paragraph, I can now see this is probably a terrible idea. I'm going to do it anyway! Here's an example of how such a layout might be declared:
layout { pins { sidepane { c 10 }, # c x = column min_horizontal_offset stack { r 50 } # r y = row min_vertical_offset }, elements { sidebar { # each side defaults to $window_<side> right $sidepane }, low_panel { left $sidepane, top $stack }, high_panel { left $sidepane, bottom $stack } } }
I think this kind of definition could have two modes: an automatic one which traverses and corrects as needed (mentioned above), and a manual one which requires using variables in the coordinates of the pins to achieve correct scaling.
Text Width
Read above- oops!
Smooth Scaling
When gradually scaling a window in a single direction, the layout should not flicker between different scaling targets. In other words, each update should also keep a consistent direction where possible. I'm having a hard time visualizing how to solve this one, so testing is needed before any hypotheses can be made.
Focus Consistency
Most interactive TUIs have you to focus on a given element before interacting with its contents. This generally involves using tab or directional keys to navigate in a given order. This order should be preserved regardless of scaling so that the user doesn't get confused and their muscle memory for using the program isn't invalidated.
Implementation
Now I'm off to program all this and see what works and what doesn't. I'll most likely end up using more traditional methods in the long run, but what good is development without experimentation? Results dissection to come (probably).ts dissection to come (probably).
Source