Lists, tables and trees
These widgets show collections that can be far larger than the window. They do not create a widget per item. Instead you give them three functions:
- length: how many items there are;
- create: build one template widget, such as a label;
- update: fill a template with the data for one item.
The widget creates only as many templates as fit on screen, and reuses them as
the user scrolls. So update must set everything that differs between items,
and must not change the collection widget itself. When your data changes,
call Refresh(), or RefreshItem(id) for a single item.
Each of these widgets scrolls by itself, so do not put it in a
container.NewScroll. Give it space with a layout that stretches it, such as
the centre of container.NewBorder.
Which one to use
| Your data is | Use |
|---|---|
| A flat list of rows | List |
| Rows and columns | Table |
| Equally sized tiles, such as photos, that wrap to the width | GridWrap |
| A hierarchy you already hold in your own structures | Tree |
| A hierarchy the user edits: rename, drag to reorder, delete | TreeView |
| Files and folders on disk | FileTree |
List
list := widget.NewList(
func() int { return len(mails) },
func() core.CanvasObject {
// A template row; it is reused for many items.
return container.NewHBox(widget.NewIcon(theme.MailComposeIcon()), widget.NewLabel(""))
},
func(id widget.ListItemID, row core.CanvasObject) {
row.(*bellui.Container).Objects[1].(*widget.Label).SetText(mails[id])
},
)
list.OnSelected = func(id widget.ListItemID) {
status.SetText("Opened " + mails[id])
}List stacks
rows vertically. Every row takes the template’s height, unless you set a
different one with SetItemHeight(id, height).
OnSelectedandOnUnselectedreport taps.Select,UnselectandUnselectAllchange the selection from code. The arrow keys move through a focused list.ScrollTo(id),ScrollToTop()andScrollToBottom()scroll from code.HideSeparatorsremoves the lines between rows.
NewListWithData takes a binding.DataList instead of the length and update
functions. To drive a list from a sortable, filterable model, see
BindListModel in Data binding.
Table
table := widget.NewTable(
func() (int, int) { return len(rows), len(headers) },
func() core.CanvasObject { return widget.NewLabel("template text") },
func(id widget.TableCellID, cell core.CanvasObject) {
cell.(*widget.Label).SetText(rows[id.Row][id.Col])
},
)
table.ShowHeaderRow = true
table.CreateHeader = func() core.CanvasObject {
return widget.NewLabelWithStyle("", bellui.TextAlignLeading, bellui.TextStyle{Bold: true})
}
table.UpdateHeader = func(id widget.TableCellID, cell core.CanvasObject) {
if id.Row == -1 { // the header row
cell.(*widget.Label).SetText(headers[id.Col])
}
}
table.SetColumnWidth(0, 120)
table.OnSelected = func(id widget.TableCellID) {
w.SetTitle("Selected " + rows[id.Row][0])
}Table
works like a list in two directions. length returns rows and columns, and
update receives a
TableCellID
with Row and Col. Columns take the template’s width; make the template
label’s text as long as a typical value, or call SetColumnWidth.
SetRowHeight does the same for rows.
ShowHeaderRowandShowHeaderColumnadd headers that stay in place while the content scrolls. By default they show “A, B, C” and “1, 2, 3”. To show your own, set bothCreateHeaderandUpdateHeader. InUpdateHeader, a header-row cell hasRow-1 and a header-column cell hasCol-1.NewTableWithHeadersturns both default headers on.StickyRowCountandStickyColumnCountkeep the first data rows or columns in place as well.OnSelectedreports the tapped cell.ScrollTo(cell)scrolls without selecting.
A table has no built-in sorting. For sortable columns over a model, see
BindTableModel
and BindTableSort.
GridWrap
photos := widget.NewGridWrap(
func() int { return 200 },
func() core.CanvasObject {
return container.NewVBox(widget.NewIcon(theme.MediaPhotoIcon()), widget.NewLabel("IMG_0000"))
},
func(id widget.GridWrapItemID, tile core.CanvasObject) {
tile.(*bellui.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("IMG_%04d", id))
},
)
photos.OnSelected = func(id widget.GridWrapItemID) {
w.SetTitle(fmt.Sprintf("Photo %d of %d columns", id, photos.ColumnCount()))
}GridWrap
has the same functions and selection methods as List, but lays its items
out in rows that wrap to the widget’s width. ColumnCount() says how many
columns currently fit. NewGridWrapWithData takes a binding.DataList.
Tree
Tree shows a
hierarchy. Every node has a unique string ID (TreeNodeID), and the root’s ID
is the empty string "" unless you set Root. NewTree takes four
functions: the child IDs of a node, whether a node is a branch, and the usual
create and update, which are told whether the node is a branch.
For a small, fixed tree, NewTreeWithStrings takes a map from each branch’s
ID to its children, with the root’s children under "". The IDs are also
shown as the text:
animals := widget.NewTreeWithStrings(map[string][]string{
"": {"Mammals", "Birds"},
"Mammals": {"Cat", "Dog"},
"Birds": {"Robin", "Owl"},
})
animals.OnSelected = func(id widget.TreeNodeID) { status.SetText(id) }
animals.OpenBranch("Mammals")OpenBranch, CloseBranch, ToggleBranch, OpenAllBranches and
CloseAllBranches expand and collapse from code. OnBranchOpened and
OnBranchClosed report the user doing it. ScrollTo(id) opens the branches
above a node and scrolls to it. NewTreeWithData takes a binding.DataTree.
TreeView
TreeView
is an MD3-styled tree that holds its own nodes. Each
TreeViewNode
has an ID, Text, an optional Icon and any Data you want to keep with
it. You add nodes with AddNode(parentID, node) (use "" for the top level)
or InsertNode, and the tree draws an icon and label for each.
project := widget.NewTreeView()
project.AllowRename = true
project.AllowReorder = true
_ = project.AddNode("", widget.TreeViewNode{ID: "src", Text: "src", Icon: theme.FolderIcon()})
_ = project.AddNode("src", widget.TreeViewNode{ID: "main.go", Text: "main.go", Icon: theme.DocumentIcon()})
_ = project.AddNode("", widget.TreeViewNode{ID: "go.mod", Text: "go.mod", Icon: theme.FileIcon()})
project.OnRenamed = func(id widget.TreeViewNodeID, name string) {
status.SetText(id + " renamed to " + name)
}
project.OpenBranch("src")Editing features are off until you turn them on:
| Field | Lets the user |
|---|---|
AllowReorder | drag a node to a new place under the same parent |
AllowReparent | drag a node into another branch |
AllowRename | double-click a node to rename it |
AllowDelete | delete a node with the Delete key or the context menu |
Each change is reported to OnMoved, OnRenamed or OnDeleted, so you can
update your own data. ShowGuideLines draws indent guides, and
ContextActions adds your own entries to a node’s right-click menu.
RenameNode, MoveNode, DeleteNode and RemoveNode make the same changes
from code.
FileTree
FileTree
is a Tree of the files and folders under a root URI. It lists folders in the
background and shows “Loading…” until each listing arrives, so a slow disk
or network share does not freeze the window. Node IDs are the URIs as
strings.
dir, _ := os.Getwd()
files := widget.NewFileTree(storage.NewFileURI(dir))
files.Watch = true
files.OnSelected = func(id widget.TreeNodeID) { status.SetText(id) }Set these fields before the tree is shown:
Filterlimits which files appear; folders always appear.ShowHiddenincludes hidden files.Sorterorders each folder’s children. Without it, the order is whatever the file system returns.Watchre-lists a folder when its contents change on disk, where the platform supports it.
Refresh() re-lists every folder the tree has loaded.