Tree View
A hierarchy whose branches open and close: folders and files, categories, a document's headings.
import com.pantrixui.components.data.treeview.PantrixTreeView
- Nodes. A
TreeNode'skeyis the same in every language, and no two nodes of a tree have one key. A row has itsiconfirst, then itslabel, and at its end a short text (trailing: a number, a file's size). - Choosing.
TreeSelection.Noneopens or closes a branch when its row is pressed.Singlechooses the row pressed;Checkboxchecks the row's checkbox. While rows are chosen, the chevron at a row's start opens and closes the branch by a press of its own. A branch's checkbox checks everything under it that can be checked, and clears it where all of it is checked; a branch with only some of what is under it checked shows a dash.onSelectionChangegives every checked node. - Disabled. A node with
enabled = falsecannot be chosen or checked and is written faint; its branch still opens and closes. - Loading when opened. A node with
hasChildren = trueand no children yet has its chevron drawn; when it is opened, the screen loads its children. While its key is inloading, a spinner stands in the chevron's place. - Filtering. While
filterholds more than spaces, the tree shows the rows whose labels hold it and the branches they stand in; the letters found are marked, and capital and small letters are compared in the theme's language (in Turkish İ with i, and I with ı). These branches are open while the filter is given, and the reader can still close them. Everything under a node that holds what is looked for shows. Where nothing is found the tree shows no row; what to say then is the screen's. - Guides.
guidesdraws a faint line down from each open branch's chevron along what stands under it. - Size. The tree is as wide as the room it is given and as tall as its open rows, and does not scroll: it stands in a page that scrolls. Every open row is composed; a tree with thousands of open rows wants a list of its own. A branch's children come and go with the theme's motion.
- Platform. A row is the platform's list row: 48dp on Android, 44pt on iOS, 32dp on desktop and the web; it is never lower than the platform's touch target, and on a desktop or a web page used by touch it takes a finger's. Each level steps in by 24dp on Android, 20pt on iOS, and 16dp on desktop and the web.
- Colour.
TreeViewColorsgives the fill, the text, the chosen row, the guides and the ground of the letters found. A tree with no fill stands on what is behind it, and on a fill it is written in the colour that fill gives. - Automation. With
tagKeythe tree isTreeView_<tagKey>, a rowTreeView_<tagKey>.Node_<key>, and its chevronTreeView_<tagKey>.Twisty_<key>. Presses are reported with the node's key asexpand,collapse,select,checkanduncheck.
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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
nodes | List<TreeNode> | the nodes at the top of the tree, in the order they are shown. No two nodes of the tree have one key. | |
expanded | Set<String> | the keys of the branches that are open. | |
onExpandedChange | (Set<String>) -> Unit | called with the keys that are to be open after a branch was opened or closed. The tree changes nothing by itself. | |
modifier | Modifier | Modifier | the tree's modifier. The tree is as wide as the room it is given and as tall as its open rows. |
selection | TreeSelection | TreeSelection.None | how rows are chosen: not at all, one at a time, or by checkboxes. |
selected | Set<String> | emptySet() | the keys of the chosen rows, or of the checked ones. |
onSelectionChange | ((Set<String>) -> Unit)? | null | called 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. |
loading | Set<String> | emptySet() | the keys of the branches whose children are being loaded: a spinner stands in their twisty's place. |
filter | String | "" | what the rows' labels are to hold for the rows to be shown, or nothing for all of them. |
guides | Boolean | false | whether a faint line runs down from each open branch's twisty along what it holds. |
style | TreeViewStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
colors | TreeViewColors? | null | this tree's own colours, where they are not the theme's: the ones set here, with the theme's for the rest. |
tagKey | String? | null | the 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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
shape | ShapeSpec | ShapeSpec.Unspecified | The corners of a chosen row's fill: a step of the brand's shape scale, or a fixed shape. |
rowHeight | Dp | Dp.Unspecified | The least height of a row. A larger type makes it taller. |
indent | Dp | Dp.Unspecified | How far a level steps in from the one it stands under. |
iconSize | Dp | Dp.Unspecified | The width and height of the twisty, of a node's own icon and of the spinner of a branch that loads. |
gap | Dp | Dp.Unspecified | Between the twisty, the checkbox, the icon, the label and what stands at the row's end. |
horizontalPadding | Dp | Dp.Unspecified | The room between a row's sides and what it holds. |
guideWidth | Dp | Dp.Unspecified | The 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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
container | Color | Color.Unspecified | The fill behind the rows. None: the tree stands on what is behind it. |
content | Color | Color.Unspecified | A 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. |
selectedContainer | Color | Color.Unspecified | The fill behind the chosen row. The primary role's container. |
selectedContent | Color | Color.Unspecified | The chosen row's label, icon and twisty: the colour that reads on selectedContainer. The primary role's onContainer. |
guide | Color | Color.Unspecified | The 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. |
match | Color | Color.Unspecified | What 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
- Keyboard. The tree is one stop for Tab. The arrows down and up go from row to row, Home and End to the first and the last row. The arrow along the row opens a branch, and on an open branch goes to its first child; the other arrow closes an open branch, and on another row goes to the branch over it. A letter goes to the next row that starts with it, and
*opens every branch beside it. Enter and Space press the row. - Screen reader. A row says its name, what stands at its end, how many children a closed branch has and at which level it stands, in the theme's words; what a press will do (choose, check, open or close) is offered, and a branch says whether it is open.