Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: Tree view widget #181

Open
FelixZY opened this issue Jul 4, 2024 · 8 comments
Open

Feature request: Tree view widget #181

FelixZY opened this issue Jul 4, 2024 · 8 comments

Comments

@FelixZY
Copy link

FelixZY commented Jul 4, 2024

I would like to render a tree view, similar to the linux tree command:

$ tree hotel/
hotel/
├── floor 1
│   ├── room 100
│   ├── room 101
│   └── room 102
└── floor 2
    ├── room 200
    ├── room 201
    └── room 202
@ajalt
Copy link
Owner

ajalt commented Jul 5, 2024

I think that's a good fit for a built-in widget: it's not trivial to make with a grid, and it's a common enough CLI pattern.

Let's collect some more requirements before we start implementing: are there any other tools that display tree-like data that we could take inspiration from? For example, should the output be colored by default, do tree entries have descriptions etc.

@FelixZY
Copy link
Author

FelixZY commented Jul 5, 2024

I don't have good answers for most of the questions posed and am not experienced enough with mordant to know if coloured by default is a good fit. I'll try to bring a few things to the discussion though:

With regards to existing tools, tree is the obvious example. I believe exa has tree layout support as well.

Syntax wise, I'm thinking something like what Daisy UI does for their menu with submenu component would be simplest. Translated to DSL, perhaps something like

tree {
  leaf {
  }
  tree {
    leaf {
    }
  }
}

Personally I'd love multi-line support for "leaves". I think that's more uncommon when looking at other tree-like tools but it would be great for many use-cases.

Being able to show other widgets, such as tables, inside a leaf would be dreamy but then again, perhaps not as hard to implement as one would think if multi-line leaves are supported.

@FelixZY
Copy link
Author

FelixZY commented Jul 6, 2024

Also, terminology should probably be based on the tree data structure.

@ajalt
Copy link
Owner

ajalt commented Jul 6, 2024

I'm noticing a symmetry in the structure: nested nodes are drawn with exactly the same characters as the top level tree. So if each entry of the tree can be a widget, you wouldn't need to differentiate between leaf and middle nodes in a dsl, you could do something like

class Entry(title: String, content: Widget? = null)
class Tree(vararg entries: Entry): Widget

And the have the class definition like

    Tree(
        Entry("bin", Tree(
            Entry("parse-ansi-codes.rs"),
        )),
        Entry("Cargo.lock"),
        Entry("Cargo.toml"),
        Entry("README.md"),
        Entry("src", Tree(
            Entry("cursor.rs"),
            Entry("lib.rs"),
            Entry("style.rs"),
        )),
        Entry("target", Tree(
            Entry("debug"),
        )),
        Entry("test"),
    )

with a dsl like

    tree {
        entry("bin", tree {
            entry("parse-ansi-codes.rs")
        })
        entry("Cargo.lock")
        entry("Cargo.toml")
        entry("README.md")
        entry("src",
            tree {
                entry("cursor.rs")
                entry("lib.rs")
                entry("style.rs")
            }
        }
        entry("target", tree {
            entry("debug")
        })
        entry("test")
    }

@FelixZY
Copy link
Author

FelixZY commented Jul 6, 2024

I'm not sure I'm a fan of forcing a title: String on every entry. Also, I think it would be nice if tree could be used without wrapping it in an entry. Example:

data class Widget(val text: String)

sealed interface TreeNode

data class Leaf(val content: Widget) : TreeNode

data class Tree(val content: Widget? = null, val entries: List<TreeNode>) : TreeNode {
    constructor(content: Widget? = null, vararg entries: TreeNode) : this(content, entries.toList())
    
    data class Builder(val content: Widget? = null, var entries: MutableList<TreeNode> = mutableListOf<TreeNode>()) {
        fun tree(content: Widget? = null, init: Tree.Builder.() -> Unit) {
            entries.add(
    			Tree.Builder(content).apply { init() }.build()
            )
        }
            
        fun leaf(init: () -> Widget) {
            entries.add(Leaf(init()))
        }

        fun build(): Tree = Tree(content, entries)
    }
}

fun tree(content: Widget? = null, init: Tree.Builder.() -> Unit): Tree =
    Tree.Builder(content).apply { init() }.build()

fun main() {
    val normalTree = Tree(
        content = Widget("Hotel"),
        Tree(
            content = Widget("Floor 1"),
            Leaf(Widget("Room 100")),
            Leaf(Widget("Room 101")),
            Leaf(Widget("Room 102"))
        ),
        Tree(
            content = Widget("Floor 2"),
            Leaf(Widget("Room 200")),
            Leaf(Widget("Room 201")),
            Leaf(Widget("Room 202"))
        )
    )
    
    val dslTree = tree(Widget("Hotel")) {
        tree(Widget("Floor 1")) {
            leaf {
                Widget("Room 100")
            }
            leaf {
                Widget("Room 101")
            }
            leaf {
                Widget("Room 102")
            }
        }
        tree(Widget("Floor 2")) {
            leaf {
                Widget("Room 200")
            }
            leaf {
                Widget("Room 201")
            }
            leaf {
                Widget("Room 202")
            }
        }
    }
    
    println(normalTree)
    println(dslTree)
    println(normalTree == dslTree)
}

Playground link: https://pl.kotl.in/o-95F5FZ1


Note that data class Widget(val text: String) is a standin for mordant's Widget class, allowing the above code to compile.

@ajalt
Copy link
Owner

ajalt commented Jul 6, 2024

Thanks for all the feedback! I'll definitely have overloads for specifying entries as either a string or a widget, like I do for all the other widget builders.

The reason I think that a list of entries is preferable than a split content/entires, is because a tree might have only leaf nodes (or more than one root) like this:

├─ Cargo.lock
├─ Cargo.toml
└─ test

And a single separate content doesn't fit well in that case.

@FelixZY
Copy link
Author

FelixZY commented Jul 6, 2024

Not sure I see the problem - wouldn't that just be

tree {
  leaf { "Cargo.lock" }
  leaf { "Cargo.toml" }
  leaf { "test" }
}

?

EDIT

I.e.

[content]
├─ [entries[0]]
├─ [entries[1]]
└─ [entries[2]]

or

[root.content]
├── [root.entries[0].content]
│   ├── [root.entries[0].entries[0]]
│   ├── [root.entries[0].entries[1]]
│   └── [root.entries[0].entries[2]]
└── [root.entries[1].content]
    ├── [root.entries[1].entries[0]]
    ├── [root.entries[1].entries[1]]
    └── [root.entries[1].entries[2]]

I guess index 0 could be made "special" to mean the current node's content but I think it makes sense to use explicit syntax.

EDIT2

Note that I imagine Tree.content as optional. If there is no content, I'd imagine the "root" of the relevant subtree (or root tree) to render as an empty space or possibly or ./similar.

@FelixZY
Copy link
Author

FelixZY commented Jul 6, 2024

How do you mean a tree could have more than one root?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants