Saturday, July 27, 2024
Google search engine
HomeUncategorizedFake Trees: Using Indents for Simpler UIs

Fake Trees: Using Indents for Simpler UIs

Created: 2023-12-29

This is a technique I’ve used in actual for-pay software, but
most recently in my text adventure game editor, Hiss.

The problem: you want a tree-like list in your UI

Let’s say you have this fantastic dynamic list of Foo and Bar data and you
would like to display it in a tree view of some sort in your application:

My Foos
    Foo 1
        Foo 1.a
        Foo 1.b
    Foo 2
    Foo 3
My Bars
    Bar I
        Baz baz baz
    Bar II
    Bar III
    Bar IV
...

This obviously you need to implement some sort of parent-child relationship
between these items, right?

That sounds like a lot of work and potentially awkward-to-work-with data
structure, especially if stored in a database. But surely there is no way
around it. It’s “obviously” the only way to go.

Okay, so let’s do that. You could express this with a parent ID.
Something like this, if you’re using a database:

  id | sort | name    | parent
 ----+------+---------+-------
  13 | 1    | Foo     | null
  78 | 1    | Foo 1   | 13
  98 | 1    | Foo 1.a | 78
  99 | 2    | Foo 1.b | 78
  ...

Now how to do we work with this?

Well, for example, one way to get tree-structured data from a relational
database with a SQL query is to write a recursive CTE (Common Table
Expressions), which are just as fun as they sound.

If you’re a programmer, you’re probably already thinking about how you would
display this data…​

Wait, but does your data really need to be a tree?

Do your items actually have to have a parent-child relationship, or do they
just need to look like they do?

I find that these lists are often just formatted this way for human
organization. There’s no actual need for a formal relationship between them.

If that’s the case, a vastly simpler method for storing this tree is:

  id | sort | indent | name
 ----+------+--------+-------
  13 | 1    | 0      | Foo
  78 | 2    | 1      | Foo 1
  98 | 3    | 2      | Foo 1.a
  99 | 4    | 2      | Foo 1.b
  ...

Why is this simpler? Because it literally describes what you see on the screen.
Which means it’s easy as pie to render. Note how the sort order is now absolute
for the entire list, not for sub-item order. The indent is quite literally how
much space to put before the item.

And it’s not just display the list that becomes easier – making an interface to
edit this tree can also be much simpler now. You can simply let the user
move items up and down and indent/unindent them to their heart’s content (or
put in some simple rules to enforce correct indenting – that’s up to you).
It becomes more like editing a list in a word processor than interfacing with
a data structure straight out of a computer science text book.

Another example using “namespacing”

I’ve got items with these names from a silly example game:

Start
banana
banana.eat
banana.peel
banana.sniff
banana.throw away
monkey
...

And these render in the interface like so:

a things list contains sub-items under the banana thing: eat

It appears I implemented some sort of namespacing feature to HissScript.

But I didn’t!

I’m just sorting the items alphabetically and if I see a dot (“.”) in a name, I
chop off the first part and indent the item.

As with the Foo and Bar list in the previous example, it looks like the game
editor “understands” the relationship, but that’s just an illusion.

Just to give a sense of how simple this is, here’s the entirety of the list
drawing code (JavaScript):

var names = Object.keys(objs).sort();

return names.map(function(n){
    if(m = /^.+(..+)$/.exec(n)){
        return ["a.indent", {onclick:link(n)}, m[1]];
    }
    return ["a", {onclick:link(n)}, n];
});

And that’s raw editor code that sets up click handlers and everything. The
pseudocode logic is just:

names = sort(things.keys)
each names as name:
    if contains('.', name):
        print_indent chop_before('.', name)
    else
        print name

(I later added a check to make sure a “parent” item with the given prefix
exists before doing the indent/chop. But that was just another couple lines.
Also, it would be no problem to add arbitrarily deep nesting levels to this
namespacing feature but I’m waiting for an actual need.)

The “namespacing” appearance is very important for a human trying to organize
a game. But it means nothing to the game editor and player. Names with dots
in them are just names. The namespacing keeps them unique, but that’s it.

I learned the hard way long ago: more often than not, people don’t
actually want (or need) trees, they just need the appearance of them.

Bonus tree-as-list

Update: Dave Long writes:

“Low-tech real trees: store path + info in a flat list (like the output of find).”

That’s definitely true and is essentially the insight behind my “banana.eat”
example above. Here’s some fake find output:

./foo/zonk
./foo/bonk
./bar/boop/bop
./bar/boop/bleep

“Want a depth-first traversal? lexically sort by the path.”

For sure:

./bar/boop/bleep
./bar/boop/bop
./foo/bonk
./foo/zonk

“Want a breadth-first traversal? sort by the (properly padded) reversal of the path”

Hmmm…​let’s see:

bleep/boop/bar
bonk/foo
bop/boop/bar
zonk/foo

That’s interesting.

Physical analogy

Just to hammer on the point, let’s say you’re working on your personal
scrapbook. You’re starting to put boxes of photos, notes, postcards, ticket stubs,
etc. into some sort of order. You’re laying everything out on the floor and
you’re making some really nice groupings of items and you’ve even made little
folded paper signs to label the groups.

Even though it is glaringly obvious to you, the human, how these groups work,
is there actually any physical mechanism built into your floor to enforce this
relationship? Of course not. The floor has no idea what’s going on.

(Or if it does, you have accidentally moved into a home built
over an ancient burial ground and are in immediate danger. Pack a bag and
leave now. Sleep in a hotel tonight and you can think about how to get your
belongings and find a new home when you’ve got a clear head tomorrow morning.
The scrapbook can wait for a better time when you’re less likely to be eaten
by skeletons.)

Caution: One size does not fit all

I’m sure there are infinite variations on this and I’m sure I’m not the first
person to think either of the above two example techniques. But I have never
seen anyone mention it before. So anything like this will probably count as a
“hack” in most programming circles.

You will almost definitely need to heavily adapt it to your specific scenario.

And for the love of all that is good, if you actually need a tree, use a
tree. Do the whole parent ID thing (or separate parent/child join table or
whatever makes sense to your data model).

In the physical analogy above, if you’re cataloging a massive research project
and you need the organizing power of physical filing cabinets and
neatly-labeled folders, the floor method is not going to cut it!
(Just make sure you’re not using haunted filing cabinets.)

Do not hack something with indents or counting symbols in strings or some
nonsense like that if you will ever need to actually know the relationship
between your items. Yes, you can do it, but it will be a path of suffering
for the lifetime of the project and all of its maintenance forever after.
Don’t say I didn’t warn you.

Read More

RELATED ARTICLES

2 COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments