Skip to content
Lists, tables and trees

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:

  1. length: how many items there are;
  2. create: build one template widget, such as a label;
  3. 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 isUse
A flat list of rowsList
Rows and columnsTable
Equally sized tiles, such as photos, that wrap to the widthGridWrap
A hierarchy you already hold in your own structuresTree
A hierarchy the user edits: rename, drag to reorder, deleteTreeView
Files and folders on diskFileTree

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).

  • OnSelected and OnUnselected report taps. Select, Unselect and UnselectAll change the selection from code. The arrow keys move through a focused list.
  • ScrollTo(id), ScrollToTop() and ScrollToBottom() scroll from code.
  • HideSeparators removes 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.

  • ShowHeaderRow and ShowHeaderColumn add 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 both CreateHeader and UpdateHeader. In UpdateHeader, a header-row cell has Row -1 and a header-column cell has Col -1.
  • NewTableWithHeaders turns both default headers on.
  • StickyRowCount and StickyColumnCount keep the first data rows or columns in place as well.
  • OnSelected reports 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:

FieldLets the user
AllowReorderdrag a node to a new place under the same parent
AllowReparentdrag a node into another branch
AllowRenamedouble-click a node to rename it
AllowDeletedelete 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:

  • Filter limits which files appear; folders always appear.
  • ShowHidden includes hidden files.
  • Sorter orders each folder’s children. Without it, the order is whatever the file system returns.
  • Watch re-lists a folder when its contents change on disk, where the platform supports it.

Refresh() re-lists every folder the tree has loaded.