Pantrix UI

Tree View

A hierarchy whose branches open and close: folders and files, categories, a document's headings.

import com.pantrixui.components.data.treeview.PantrixTreeView

Examples

Folders and files

A project's folders and files, one of them chosen. A press on a row chooses it; the chevron before a folder opens it or closes it. Each file's size is written at the row's end. The open folders and the chosen file are the screen's state and are kept with rememberSaveable.

var open by rememberSaveable { mutableStateOf(setOf("app", "app/src")) }
var chosen by rememberSaveable { mutableStateOf(setOf("app/src/App.kt")) }
PantrixTreeView(
    nodes = PROJECT_FILES,
    expanded = open,
    onExpandedChange = { open = it },
    selection = TreeSelection.Single,
    selected = chosen,
    onSelectionChange = { chosen = it },
    tagKey = "files"
)
PantrixText(
    text = "Chosen: ${chosen.firstOrNull() ?: "none"}",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary
)

private val PROJECT_FILES = listOf(
    TreeNode(
        key = "app",
        label = "app",
        icon = PantrixIcons.Folder,
        children = listOf(
            TreeNode(
                key = "app/src",
                label = "src",
                icon = PantrixIcons.Folder,
                children = listOf(
                    TreeNode(key = "app/src/App.kt", label = "App.kt", trailing = "4 KB"),
                    TreeNode(key = "app/src/Theme.kt", label = "Theme.kt", trailing = "2 KB"),
                    TreeNode(key = "app/src/Screens.kt", label = "Screens.kt", trailing = "9 KB")
                )
            ),
            TreeNode(
                key = "app/test",
                label = "test",
                icon = PantrixIcons.Folder,
                children = listOf(TreeNode(key = "app/test/AppTest.kt", label = "AppTest.kt", trailing = "3 KB"))
            ),
            TreeNode(key = "app/build.gradle.kts", label = "build.gradle.kts", trailing = "1 KB")
        )
    ),
    TreeNode(
        key = "docs",
        label = "docs",
        icon = PantrixIcons.Folder,
        children = listOf(
            TreeNode(key = "docs/README.md", label = "README.md", trailing = "6 KB"),
            TreeNode(key = "docs/CHANGELOG.md", label = "CHANGELOG.md", trailing = "12 KB")
        )
    ),
    TreeNode(key = "settings.gradle.kts", label = "settings.gradle.kts", trailing = "1 KB")
)

Permissions with checkboxes

Permissions with checkboxes. Checking a branch checks every permission under it; a branch with only some of them checked shows a dash. "Delete users" is disabled: it cannot be checked, its branch does not change it, and because of it the Users branch is never wholly checked.

var open by rememberSaveable { mutableStateOf(setOf("users", "billing")) }
var granted by rememberSaveable { mutableStateOf(setOf("billing.view")) }
PantrixTreeView(
    nodes = PERMISSIONS,
    expanded = open,
    onExpandedChange = { open = it },
    selection = TreeSelection.Checkbox,
    selected = granted,
    onSelectionChange = { granted = it },
    tagKey = "permissions"
)
PantrixText(
    text = "Checked: ${granted.size}",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary
)

private val PERMISSIONS = listOf(
    TreeNode(
        key = "users",
        label = "Users",
        children = listOf(
            TreeNode(key = "users.view", label = "View users"),
            TreeNode(key = "users.edit", label = "Edit users"),
            TreeNode(key = "users.delete", label = "Delete users", enabled = false)
        )
    ),
    TreeNode(
        key = "billing",
        label = "Invoices",
        children = listOf(
            TreeNode(key = "billing.view", label = "View invoices"),
            TreeNode(key = "billing.pay", label = "Pay an invoice"),
            TreeNode(key = "billing.refund", label = "Refund")
        )
    ),
    TreeNode(
        key = "settings",
        label = "Settings",
        children = listOf(
            TreeNode(key = "settings.general", label = "General"),
            TreeNode(key = "settings.security", label = "Security")
        )
    )
)

Filtering and guides

filter looks for what is typed in the field in the labels: the rows found and the branches they stand in show, and the letters found are marked. Capital and small letters are compared in the theme's language: in Turkish, typing "ı" finds "Işıklandırma" (lighting). The guides (guides) link what stands under an open branch.

val query = rememberTextFieldState()
var open by rememberSaveable { mutableStateOf(setOf("home")) }
PantrixTextField(
    state = query,
    modifier = Modifier.fillMaxWidth(),
    placeholder = "Search the categories",
    showClearButton = true,
    contentDescription = "Search the categories",
    tagKey = "category_search"
)
PantrixTreeView(
    nodes = CATEGORIES,
    expanded = open,
    onExpandedChange = { open = it },
    filter = query.text.toString(),
    guides = true,
    tagKey = "categories"
)

private val CATEGORIES = listOf(
    TreeNode(
        key = "home",
        label = "Home and living",
        children = listOf(
            TreeNode(key = "home.kitchen", label = "Kitchen", trailing = "128"),
            TreeNode(key = "home.light", label = "Lighting", trailing = "64"),
            TreeNode(key = "home.bath", label = "Bathroom", trailing = "41")
        )
    ),
    TreeNode(
        key = "tech",
        label = "Electronics",
        children = listOf(
            TreeNode(key = "tech.phone", label = "Phones", trailing = "212"),
            TreeNode(
                key = "tech.computer",
                label = "Computers",
                children = listOf(
                    TreeNode(key = "tech.computer.laptop", label = "Laptops", trailing = "96"),
                    TreeNode(key = "tech.computer.desktop", label = "Desktops", trailing = "38"),
                    TreeNode(key = "tech.computer.tablet", label = "Tablets", trailing = "57")
                )
            )
        )
    ),
    TreeNode(
        key = "wear",
        label = "Clothing",
        children = listOf(
            TreeNode(key = "wear.women", label = "Women", trailing = "340"),
            TreeNode(key = "wear.men", label = "Men", trailing = "295"),
            TreeNode(key = "wear.inner", label = "Underwear", trailing = "87")
        )
    )
)

Branches loaded when they open

Branches loaded when they open. The folders' children are not known at first (hasChildren = true, children empty); when a folder is opened the screen puts it in loading and loads its children, and meanwhile a spinner stands in its chevron's place. Here the loading is mimicked by waiting half a second.

var open by rememberSaveable { mutableStateOf(emptySet<String>()) }
var loaded by rememberSaveable { mutableStateOf(emptyMap<String, Int>()) }
var loading by rememberSaveable { mutableStateOf(emptySet<String>()) }
LaunchedEffect(loading) {
    if (loading.isNotEmpty()) {
        delay(500)
        loaded = loaded + loading.associateWith { 3 }
        loading = emptySet()
    }
}
PantrixTreeView(
    nodes = listOf("documents" to "Documents", "pictures" to "Pictures", "music" to "Music").map { (key, label) ->
        lazyFolder(key, label, loaded)
    },
    expanded = open,
    onExpandedChange = { next ->
        loading = loading + (next - open).filter { it !in loaded }
        open = next
    },
    loading = loading,
    tagKey = "lazy"
)

private fun lazyFolder(
    key: String,
    label: String,
    loaded: Map<String, Int>
): TreeNode = TreeNode(
    key = key,
    label = label,
    icon = PantrixIcons.Folder,
    hasChildren = true,
    children = (1..(loaded[key] ?: 0)).map { TreeNode(key = "$key/$it", label = "$label $it") }
)

A document's headings

A tree with no choosing, a document's headings: a press on a row opens the section or closes it. With the keyboard the tree is reached by Tab; the arrows walk the rows, a letter goes to the heading that starts with it, and * opens every section beside it.

var open by rememberSaveable { mutableStateOf(setOf("setup")) }
PantrixTreeView(
    nodes = OUTLINE,
    expanded = open,
    onExpandedChange = { open = it },
    guides = true,
    tagKey = "outline"
)

private val OUTLINE = listOf(
    TreeNode(key = "intro", label = "Introduction"),
    TreeNode(
        key = "setup",
        label = "Setup",
        children = listOf(
            TreeNode(key = "setup.gradle", label = "Gradle"),
            TreeNode(key = "setup.theme", label = "Theme")
        )
    ),
    TreeNode(
        key = "parts",
        label = "Components",
        children = listOf(
            TreeNode(key = "parts.buttons", label = "Buttons"),
            TreeNode(
                key = "parts.data",
                label = "Data",
                children = listOf(
                    TreeNode(key = "parts.data.table", label = "Table"),
                    TreeNode(key = "parts.data.tree", label = "Tree")
                )
            )
        )
    ),
    TreeNode(key = "faq", label = "Frequently asked")
)

On a fill, with sizes of its own

On a fill and with sizes of its own: the tree has no fill of its own, and on the primary role's soft fill it is written in the colour that fill gives. TreeViewStyle makes the rows lower and the indent larger.

var open by rememberSaveable { mutableStateOf(setOf("setup", "parts")) }
PantrixSurface(color = PantrixTheme.colors.primary.container) {
    PantrixTreeView(
        nodes = OUTLINE,
        expanded = open,
        onExpandedChange = { open = it },
        guides = true,
        style = TreeViewStyle(rowHeight = 36.dp, indent = 28.dp),
        tagKey = "on_a_fill"
    )
}

private val OUTLINE = listOf(
    TreeNode(key = "intro", label = "Introduction"),
    TreeNode(
        key = "setup",
        label = "Setup",
        children = listOf(
            TreeNode(key = "setup.gradle", label = "Gradle"),
            TreeNode(key = "setup.theme", label = "Theme")
        )
    ),
    TreeNode(
        key = "parts",
        label = "Components",
        children = listOf(
            TreeNode(key = "parts.buttons", label = "Buttons"),
            TreeNode(
                key = "parts.data",
                label = "Data",
                children = listOf(
                    TreeNode(key = "parts.data.table", label = "Table"),
                    TreeNode(key = "parts.data.tree", label = "Tree")
                )
            )
        )
    ),
    TreeNode(key = "faq", label = "Frequently asked")
)

In its own colours

In its own colours: TreeViewColors paints the tree's ground, its text, the selected row, the indent guides and the text a search found. Here the selected row is in the colour of information, not the brand's.

var open by rememberSaveable { mutableStateOf(setOf("app")) }
var chosen by rememberSaveable { mutableStateOf(setOf("app/build.gradle.kts")) }
PantrixTreeView(
    nodes = PROJECT_FILES,
    expanded = open,
    onExpandedChange = { open = it },
    selection = TreeSelection.Single,
    selected = chosen,
    onSelectionChange = { chosen = it },
    colors = TreeViewColors(
        selectedContainer = PantrixTheme.colors.info.container,
        selectedContent = PantrixTheme.colors.info.onContainer,
        guide = PantrixTheme.colors.info.color
    ),
    tagKey = "info_files"
)

private val PROJECT_FILES = listOf(
    TreeNode(
        key = "app",
        label = "app",
        icon = PantrixIcons.Folder,
        children = listOf(
            TreeNode(
                key = "app/src",
                label = "src",
                icon = PantrixIcons.Folder,
                children = listOf(
                    TreeNode(key = "app/src/App.kt", label = "App.kt", trailing = "4 KB"),
                    TreeNode(key = "app/src/Theme.kt", label = "Theme.kt", trailing = "2 KB"),
                    TreeNode(key = "app/src/Screens.kt", label = "Screens.kt", trailing = "9 KB")
                )
            ),
            TreeNode(
                key = "app/test",
                label = "test",
                icon = PantrixIcons.Folder,
                children = listOf(TreeNode(key = "app/test/AppTest.kt", label = "AppTest.kt", trailing = "3 KB"))
            ),
            TreeNode(key = "app/build.gradle.kts", label = "build.gradle.kts", trailing = "1 KB")
        )
    ),
    TreeNode(
        key = "docs",
        label = "docs",
        icon = PantrixIcons.Folder,
        children = listOf(
            TreeNode(key = "docs/README.md", label = "README.md", trailing = "6 KB"),
            TreeNode(key = "docs/CHANGELOG.md", label = "CHANGELOG.md", trailing = "12 KB")
        )
    ),
    TreeNode(key = "settings.gradle.kts", label = "settings.gradle.kts", trailing = "1 KB")
)

API

nodes

A hierarchy whose branches open and close: folders and files, categories, a document's headings.

ParameterTypeDefaultWhat it is
nodesList<TreeNode>the nodes at the top of the tree, in the order they are shown. No two nodes of the tree have one key.
expandedSet<String>the keys of the branches that are open.
onExpandedChange(Set<String>) -> Unitcalled with the keys that are to be open after a branch was opened or closed. The tree changes nothing by itself.
modifierModifierModifierthe tree's modifier. The tree is as wide as the room it is given and as tall as its open rows.
selectionTreeSelectionTreeSelection.Nonehow rows are chosen: not at all, one at a time, or by checkboxes.
selectedSet<String>emptySet()the keys of the chosen rows, or of the checked ones.
onSelectionChange((Set<String>) -> Unit)?nullcalled with the keys that are to be chosen, or checked, after a press. null leaves the rows as selected has them, and a press on a row opens its branch or closes it.
loadingSet<String>emptySet()the keys of the branches whose children are being loaded: a spinner stands in their twisty's place.
filterString""what the rows' labels are to hold for the rows to be shown, or nothing for all of them.
guidesBooleanfalsewhether a faint line runs down from each open branch's twisty along what it holds.
styleTreeViewStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsTreeViewColors?nullthis tree's own colours, where they are not the theme's: the ones set here, with the theme's for the rest.
tagKeyString?nullthe tree's automation key: the tree is TreeView_<tagKey>, a row in it TreeView_<tagKey>.Node_<key> and its twisty TreeView_<tagKey>.Twisty_<key>. What a press does is reported as a TreeView event: expand, collapse, select, check and uncheck, with the node's key.

TreeViewStyle

PantrixTreeView's geometry: how tall a row is, how far each level steps in, and how large the twisty is.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe corners of a chosen row's fill: a step of the brand's shape scale, or a fixed shape.
rowHeightDpDp.UnspecifiedThe least height of a row. A larger type makes it taller.
indentDpDp.UnspecifiedHow far a level steps in from the one it stands under.
iconSizeDpDp.UnspecifiedThe width and height of the twisty, of a node's own icon and of the spinner of a branch that loads.
gapDpDp.UnspecifiedBetween the twisty, the checkbox, the icon, the label and what stands at the row's end.
horizontalPaddingDpDp.UnspecifiedThe room between a row's sides and what it holds.
guideWidthDpDp.UnspecifiedThe width of the lines that run down from an open branch where the tree draws them.

TreeViewColors

What a PantrixTreeView is painted with, for a tree that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe fill behind the rows. None: the tree stands on what is behind it.
contentColorColor.UnspecifiedA row's label and its icon. The theme's textPrimary, or on a fill that is not the tree's, the colour that fill gives what stands on it. The twisty and what stands at a row's end are the theme's textSecondary on the theme's own grounds, and this colour elsewhere and where it is given.
selectedContainerColorColor.UnspecifiedThe fill behind the chosen row. The primary role's container.
selectedContentColorColor.UnspecifiedThe chosen row's label, icon and twisty: the colour that reads on selectedContainer. The primary role's onContainer.
guideColorColor.UnspecifiedThe lines that run down from an open branch. The theme's divider on the theme's own grounds. On a fill, where that line would not read, a share of content.
matchColorColor.UnspecifiedWhat is laid behind the letters a filter found. The theme's warning role softened, written in what the theme pairs with it; on a fill a share of what is written there, under letters that keep their colour. A row that cannot be chosen lays a share of it under letters that keep the row's faint colour.

Accessibility