Master–detail (tree)#
A hierarchy in the sidebar drives a detail view — the file explorer, the settings tree, the category browser. Like the list master–detail, but the records nest under parents.
How it works#
tree_nav builds the sidebar from a hierarchy and returns the Tree
driving it. Decorate a builder with @shell.detail to render the selected
node, received as a record dict (its label is text). A tree opens with
nothing selected, so a placeholder shows in the content area until a node is
picked.
Declare the hierarchy inline with nodes= (each a {"label", "icon",
"children", ...} spec; extra keys ride along as the node’s data):
tree = shell.tree_nav(nodes=[
{"label": "src", "icon": "folder", "children": [
{"label": "app.py", "icon": "filetype-py", "size": "4.2 KB"},
]},
])
@shell.detail
def show(node):
bs.Label(node["text"], font="heading-lg")
For large or dynamic hierarchies, pass source= instead — a flat
adjacency-list data source where each row names
its parent (tree_nav(source=files, parent_field="parent_id", label_field="name"));
the tree then loads each branch lazily as it expands.
Because tree_nav hands back the Tree, you can drive the view — the file
explorer above opens expanded with a file selected:
tree.expand_all()
app = tree.find(lambda node: node.label == "app.py")
if app is not None:
tree.select(app)
Example#
1"""Master–detail (tree) — a hierarchy drives a detail view (a file explorer).
2
3``tree_nav`` builds the sidebar from a hierarchy and returns the `Tree` driving
4it; ``@shell.detail`` renders the selected node, received as a record dict. A
5folders → file-details browser is the archetypal tree screen. Here the hierarchy
6is declared inline with ``nodes=`` (each node a ``{"label", "icon", "children",
7...}`` spec; extra keys ride along as the node's data). For large or dynamic
8hierarchies, pass ``source=`` instead — a flat adjacency-list data source where
9each row names its parent.
10
11A tree opens with nothing selected (showing the ``placeholder``); we open it
12expanded with a file selected, like a real file explorer, by driving the
13returned `Tree`.
14"""
15import bootstack as bs
16
17tree_nodes = [
18 {"label": "src", "icon": "folder", "children": [
19 {"label": "app.py", "icon": "filetype-py", "kind": "Python source", "size": "4.2 KB"},
20 {"label": "utils.py", "icon": "filetype-py", "kind": "Python source", "size": "1.8 KB"},
21 ]},
22 {"label": "tests", "icon": "folder", "children": [
23 {"label": "test_app.py", "icon": "filetype-py", "kind": "Python source", "size": "2.0 KB"},
24 ]},
25 {"label": "docs", "icon": "folder", "children": [
26 {"label": "README.md", "icon": "filetype-md", "kind": "Markdown", "size": "920 B"},
27 ]},
28 {"label": "LICENSE", "icon": "file-earmark", "kind": "Text", "size": "1.1 KB"},
29]
30
31with bs.AppShell(title="Files", size=(900, 580)) as shell:
32 shell.commandbar.add_label("Project", font="heading-md")
33 shell.commandbar.add_spacer()
34 shell.commandbar.add_button(icon="circle-half", on_click=bs.toggle_theme)
35
36 tree = shell.tree_nav(nodes=tree_nodes, placeholder="Select a file")
37
38 @shell.detail
39 def show(node):
40 with bs.VStack(fill="both", expand=True, anchor_items="w", gap=12, padding=20):
41 bs.Label(node["text"], font="heading-lg")
42 bs.Label(node.get("kind", ""), font="caption")
43 if node.get("size"):
44 bs.Label(f"Size: {node['size']}")
45
46 # Open expanded with a file selected, like a real file explorer.
47 tree.expand_all()
48 app = tree.find(lambda node: node.label == "app.py")
49 if app is not None:
50 tree.select(app)
51
52shell.run()
When to use#
Use the tree master–detail when records form a hierarchy. If the records are flat, the simpler list master–detail reads better. Note that a tree can’t collapse into the icon rail (a branch isn’t one glyph), so under a workspace rail a tree sidebar hides rather than compacts.