model

package
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

Documentation

Overview

Package model provides an observable, typed data layer that sits between an application's data and the List/Table widgets — a composable sort/filter/ selection layer in the spirit of Qt's model/view classes.

The core type is Model, a read-only observable sequence of T. SortProxy and FilterProxy wrap a Model and present a re-ordered or narrowed view that is itself a Model, so they compose (filter then sort then view). They do their O(n log n) / O(n) work once per source change and never per displayed row, so a widget bound to a proxy keeps its O(visible) per-frame cost. SelectionModel tracks selection by stable item identity rather than row index, so a selection survives a re-sort, a re-filter or an insert.

The widget adapters that drive a List or Table from a Model live in the widget package (widget.BindListModel / widget.BindTableModel), keeping this package free of any dependency on widget so the model layer is reusable on its own.

Model mutation and the proxy/selection updates it triggers follow the widgets' threading rules: mutate on the main goroutine (via bellui.Do when a change originates on a background goroutine), matching how widget.List and FileTree already expect their callbacks to be driven.

Since: 1.0

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StringLess

func StringLess(a, b string) bool

StringLess orders strings by the active locale's collation order (see lang.Compare), the recommended default comparator for sorting user-visible text: it puts (for example) German "äpfel" with "a" and Swedish "ö" after "z", where a raw code-point comparison would not. Use it with SortProxy.SetLess:

proxy.SetLess(model.StringLess)

Since: 1.0

Types

type FilterProxy

type FilterProxy[T any] struct {
	// contains filtered or unexported fields
}

FilterProxy wraps a source Model and presents only the items that satisfy a predicate, while remaining a Model itself so it can be composed (for example wrapped in a SortProxy, or bound directly to a widget). It maintains the list of surviving source indices and rebuilds it once whenever the source changes or the predicate is replaced — an O(n) pass, never per displayed item.

Since: 1.0

func NewFilterProxy

func NewFilterProxy[T any](src Model[T]) *FilterProxy[T]

NewFilterProxy returns a proxy over src. Until a predicate is set with FilterProxy.SetPredicate every item passes.

Since: 1.0

func (*FilterProxy) AddListener

func (s *FilterProxy) AddListener(l binding.DataListener)

AddListener registers l and fires it once immediately.

func (*FilterProxy[T]) Close

func (p *FilterProxy[T]) Close()

Close detaches the proxy from its source. After Close the proxy is inert.

func (*FilterProxy[T]) Item

func (p *FilterProxy[T]) Item(i int) T

Item returns the item shown at display index i.

func (*FilterProxy[T]) Len

func (p *FilterProxy[T]) Len() int

Len reports the number of items that currently pass the predicate.

func (*FilterProxy) RemoveListener

func (s *FilterProxy) RemoveListener(l binding.DataListener)

RemoveListener detaches the first registration matching l.

func (*FilterProxy[T]) SetItem

func (p *FilterProxy[T]) SetItem(i int, item T)

SetItem writes item back to the source row that display index i currently maps to and notifies listeners. It is a no-op (with a logged error) when i is out of range or the source is not a MutableModel. See MutableModel.

Since: 1.0

func (*FilterProxy[T]) SetPredicate

func (p *FilterProxy[T]) SetPredicate(keep func(T) bool)

SetPredicate replaces the keep predicate (nil keeps every item) and re-filters. For search-as-you-type, debounce calls to this on the input side; each call is a single O(n) re-index.

func (*FilterProxy[T]) SourceIndex

func (p *FilterProxy[T]) SourceIndex(i int) int

SourceIndex maps a display index to the corresponding index in the source model, or -1 if i is out of range.

Since: 1.0

type Model

type Model[T any] interface {
	binding.DataItem

	// Len reports the number of items currently exposed by the model.
	Len() int
	// Item returns the item at display index i, where 0 <= i < Len().
	Item(i int) T
}

Model is a read-only, observable sequence of T. It reuses the binding package's listener vocabulary (binding.DataItem): a listener registered with AddListener is fired once immediately on registration and again whenever the contents change, so a widget adapter can subscribe and render the current state in one step.

Since: 1.0

func FromDataList

func FromDataList(l binding.DataList) Model[binding.DataItem]

FromDataList exposes an existing binding.DataList as a Model, forwarding its change notifications. The returned model reads through to the list, so mutate the underlying binding as usual; the model reflects it and re-notifies.

The model holds a listener on the source for its lifetime; it is meant to live as long as the binding it wraps.

Since: 1.0

type MutableModel

type MutableModel[T any] interface {
	Model[T]

	// SetItem replaces the item at display index i and notifies listeners.
	SetItem(i int, item T)
}

MutableModel is a Model whose items can be written back in place — the write counterpart to the read-only Model. It is the seam the editing delegates (widget.BindListEditing / widget.BindTableEditing) commit an edited value through.

SliceModel is always mutable. SortProxy and FilterProxy are mutable when their ultimate source is: a SetItem on a proxy maps the display index to the source row through the proxy's permutation — and through any chain of proxies (filter → sort → view) — then writes there, so editing a displayed row updates the correct underlying row regardless of the active sort or filter.

When a proxy's source is not itself a MutableModel the write cannot be forwarded. Rather than panic, SetItem logs an error via bellui.LogError and leaves the model unchanged, so a bound editor degrades to a no-op instead of crashing. Callers that must know whether a write is possible should assert the concrete source type, or wrap a SliceModel at the base of the proxy chain.

Since: 1.0

type SelectionMode

type SelectionMode int

SelectionMode controls how many items a SelectionModel may hold selected.

Since: 1.0

const (
	// SelectSingle keeps at most one item selected; selecting another replaces it.
	SelectSingle SelectionMode = iota
	// SelectMulti allows any set of items to be selected independently.
	SelectMulti
)

type SelectionModel

type SelectionModel[T any, K comparable] struct {
	// contains filtered or unexported fields
}

SelectionModel tracks the selected items of a Model by a stable identity key K rather than by row index, so a selection survives a re-sort, a re-filter or an insert in the underlying model — the index-remap bug that positional selection (widget.List/Table store an int index) suffers.

K is derived from each item by the id function passed to NewSelectionModel; choose a genuinely stable key (a database id, a file path, a struct field), not the row index — using the index reintroduces exactly the bug this type exists to avoid.

It is observable: register with AddListener to be notified (immediately, then on every change) when the selected set changes — for example to refresh a bound widget's highlight.

Since: 1.0

func NewSelectionModel

func NewSelectionModel[T any, K comparable](m Model[T], id func(T) K) *SelectionModel[T, K]

NewSelectionModel returns a selection tracker over m, keying items by id. The default mode is SelectSingle; change it with SelectionModel.SetMode.

Since: 1.0

func (*SelectionModel) AddListener

func (s *SelectionModel) AddListener(l binding.DataListener)

AddListener registers l and fires it once immediately.

func (*SelectionModel[T, K]) Clear

func (s *SelectionModel[T, K]) Clear()

Clear removes all selection.

func (*SelectionModel[T, K]) IndexOf

func (s *SelectionModel[T, K]) IndexOf(k K) int

IndexOf returns the current display index of the item with key k in the bound model, or -1 if k is not present. This is how a widget adapter translates a stable selection back into the row to highlight after a re-sort or re-filter.

It is an O(n) scan of the model; call it when the selection or model changes, not per frame.

func (*SelectionModel[T, K]) IsIndexSelected

func (s *SelectionModel[T, K]) IsIndexSelected(i int) bool

IsIndexSelected reports whether the item at display index i is selected. A widget's UpdateItem can call this to style a row for the current selection — including multi-selection, which the built-in single-row highlight cannot show.

func (*SelectionModel[T, K]) IsSelected

func (s *SelectionModel[T, K]) IsSelected(k K) bool

IsSelected reports whether the item with key k is selected.

func (*SelectionModel[T, K]) Len

func (s *SelectionModel[T, K]) Len() int

Len reports the number of selected items.

func (*SelectionModel) RemoveListener

func (s *SelectionModel) RemoveListener(l binding.DataListener)

RemoveListener detaches the first registration matching l.

func (*SelectionModel[T, K]) Select

func (s *SelectionModel[T, K]) Select(k K)

Select adds the item with key k to the selection. In SelectSingle mode it replaces any current selection.

func (*SelectionModel[T, K]) SelectIndex

func (s *SelectionModel[T, K]) SelectIndex(i int)

SelectIndex selects the item at display index i in the bound model. It is the bridge a widget adapter uses to turn a positional tap into an identity-keyed selection.

func (*SelectionModel[T, K]) SelectRange

func (s *SelectionModel[T, K]) SelectRange(to int)

SelectRange selects every item whose current display position lies between the anchor (the last singly-selected item, or the first item if none) and the item at display index to, inclusive. This backs shift-click range selection; it is a no-op in SelectSingle mode. Keys are resolved against the model's current order, so the range reflects the active sort/filter.

func (*SelectionModel[T, K]) Selected

func (s *SelectionModel[T, K]) Selected() []K

Selected returns the selected keys in selection order.

func (*SelectionModel[T, K]) SelectedIndices

func (s *SelectionModel[T, K]) SelectedIndices() []int

SelectedIndices returns the current display indices of the selected items that are still present in the model, ascending. Items no longer in the model (for example filtered out) are omitted here but remain selected — they reappear when the filter is relaxed, which is the whole point of identity-based selection.

func (*SelectionModel[T, K]) SetMode

func (s *SelectionModel[T, K]) SetMode(mode SelectionMode)

SetMode sets single- or multi-selection. Switching to SelectSingle while several items are selected keeps only the most recently selected one.

func (*SelectionModel[T, K]) Toggle

func (s *SelectionModel[T, K]) Toggle(k K)

Toggle selects k if unselected, or unselects it if selected.

func (*SelectionModel[T, K]) ToggleIndex

func (s *SelectionModel[T, K]) ToggleIndex(i int)

ToggleIndex toggles selection of the item at display index i.

func (*SelectionModel[T, K]) Unselect

func (s *SelectionModel[T, K]) Unselect(k K)

Unselect removes k from the selection (no-op if it was not selected).

type SliceModel

type SliceModel[T any] struct {
	// contains filtered or unexported fields
}

SliceModel is a Model backed by an in-memory slice of T. Its mutators (Set, Append, SetItem) notify listeners; call them on the main goroutine.

Since: 1.0

func NewSliceModel

func NewSliceModel[T any](items ...T) *SliceModel[T]

NewSliceModel returns a SliceModel seeded with the given items.

Since: 1.0

func (*SliceModel) AddListener

func (s *SliceModel) AddListener(l binding.DataListener)

AddListener registers l and fires it once immediately.

func (*SliceModel[T]) Append

func (m *SliceModel[T]) Append(items ...T)

Append adds items to the end and notifies listeners.

func (*SliceModel[T]) Item

func (m *SliceModel[T]) Item(i int) T

Item returns the item at index i.

func (*SliceModel[T]) Len

func (m *SliceModel[T]) Len() int

Len reports the number of items.

func (*SliceModel) RemoveListener

func (s *SliceModel) RemoveListener(l binding.DataListener)

RemoveListener detaches the first registration matching l.

func (*SliceModel[T]) Set

func (m *SliceModel[T]) Set(items []T)

Set replaces the whole contents and notifies listeners.

func (*SliceModel[T]) SetItem

func (m *SliceModel[T]) SetItem(i int, item T)

SetItem replaces the item at index i and notifies listeners.

type SortProxy

type SortProxy[T any] struct {
	// contains filtered or unexported fields
}

SortProxy wraps a source Model and presents its items in a sorted order, while remaining a Model itself so it can be composed (for example wrapped in a FilterProxy, or bound directly to a widget). It maintains a permutation of source indices and rebuilds it once whenever the source changes or the comparator is replaced — never per displayed item — so a bound widget keeps its O(visible) per-frame cost.

Since: 1.0

func NewSortProxy

func NewSortProxy[T any](src Model[T]) *SortProxy[T]

NewSortProxy returns a proxy over src. Until a comparator is set with SortProxy.SetLess the proxy preserves the source order; for user-visible text pass StringLess.

Since: 1.0

func (*SortProxy) AddListener

func (s *SortProxy) AddListener(l binding.DataListener)

AddListener registers l and fires it once immediately.

func (*SortProxy[T]) Close

func (p *SortProxy[T]) Close()

Close detaches the proxy from its source. After Close the proxy is inert.

func (*SortProxy[T]) Item

func (p *SortProxy[T]) Item(i int) T

Item returns the item shown at display index i.

func (*SortProxy[T]) Len

func (p *SortProxy[T]) Len() int

Len reports the number of items (equal to the source length).

func (*SortProxy) RemoveListener

func (s *SortProxy) RemoveListener(l binding.DataListener)

RemoveListener detaches the first registration matching l.

func (*SortProxy[T]) SetItem

func (p *SortProxy[T]) SetItem(i int, item T)

SetItem writes item back to the source row that display index i currently maps to and notifies listeners. It is a no-op (with a logged error) when i is out of range or the source is not a MutableModel. See MutableModel.

Since: 1.0

func (*SortProxy[T]) SetLess

func (p *SortProxy[T]) SetLess(less func(a, b T) bool)

SetLess replaces the comparator (nil restores source order) and re-sorts.

func (*SortProxy[T]) SourceIndex

func (p *SortProxy[T]) SourceIndex(i int) int

SourceIndex maps a display index to the corresponding index in the source model, or -1 if i is out of range.

Since: 1.0

type TreeProxy

type TreeProxy struct {
	// contains filtered or unexported fields
}

TreeProxy wraps a TreeSource and presents a filtered and/or sorted view of it that is itself a TreeSource, so it can back a widget.Tree (see widget.BindTreeModel) or be composed with another proxy. It is the tree-shaped counterpart of SortProxy/FilterProxy, with two semantics unique to a hierarchy:

  • Filtering is ancestor-keeping. A node is visible when it matches the predicate itself or has any visible (matching) descendant, so a deep match stays reachable: its whole ancestor chain is kept even though those ancestors do not match. Non-matching nodes with no matching descendant are dropped. Set TreeProxy.SetKeepMatchingSubtree to additionally reveal the entire subtree under a branch that matches by its own id (file-explorer style), rather than only the paths that reach a match.

  • Sorting is per-sibling-group: the children of each node are ordered among themselves by the comparator, and the hierarchy is preserved. There is no global flattened order.

The comparator and predicate both receive a node id (the string key). When the user-visible label differs from the id, map id→label inside the closure (as StringLess with a label lookup, or via lang.Compare).

A filter change walks the source tree once (O(n)) to recompute the visible set; a sort-only change is lazy (children are sorted per parent on first access and cached). Neither does per-frame work, so a bound widget keeps its O(visible) cost. Unlike the flat proxies the source here is a raw callback tree rather than an observable Model, so when the underlying data changes call TreeProxy.Refresh to rebuild and notify.

Since: 1.0

func NewTreeProxy

func NewTreeProxy(src TreeSource) *TreeProxy

NewTreeProxy returns a proxy over src. Until a comparator is set with TreeProxy.SetLess children keep their source order, and until a predicate is set with TreeProxy.SetPredicate every node is visible.

Since: 1.0

func (*TreeProxy) AddListener

func (s *TreeProxy) AddListener(l binding.DataListener)

AddListener registers l and fires it once immediately.

func (*TreeProxy) Children

func (p *TreeProxy) Children(uid string) []string

Children returns the visible, sorted child ids of uid. The returned slice is a copy the caller may retain or mutate.

func (*TreeProxy) IsBranch

func (p *TreeProxy) IsBranch(uid string) bool

IsBranch reports whether uid should be drawn as an expandable branch. Without a filter it passes through to the source. Under a filter it reports true only when uid has at least one visible child, so the tree never shows an expand affordance that would reveal nothing.

func (*TreeProxy) Refresh

func (p *TreeProxy) Refresh()

Refresh rebuilds the view from the current source contents and fires the change listeners. Call it after the underlying tree data changes (the source is a raw callback tree, not an observable model, so the proxy cannot detect this itself).

func (*TreeProxy) RemoveListener

func (s *TreeProxy) RemoveListener(l binding.DataListener)

RemoveListener detaches the first registration matching l.

func (*TreeProxy) Root

func (p *TreeProxy) Root() string

Root returns the source root id unchanged.

func (*TreeProxy) SetKeepMatchingSubtree

func (p *TreeProxy) SetKeepMatchingSubtree(keep bool)

SetKeepMatchingSubtree controls what a branch that matches the predicate by its own id reveals. When false (the default) only the paths that reach a match are kept, so a matching branch shows just the descendants that themselves match (or lead to a match). When true the entire subtree under a matching branch is kept visible, matching the common file-explorer behaviour of "folder name matches → show its contents". It fires the change listeners.

func (*TreeProxy) SetLess

func (p *TreeProxy) SetLess(less func(a, b string) bool)

SetLess replaces the per-sibling-group comparator (nil restores source order) and re-sorts. It fires the change listeners.

func (*TreeProxy) SetPredicate

func (p *TreeProxy) SetPredicate(keep func(uid string) bool)

SetPredicate replaces the keep predicate (nil keeps every node) and re-filters with ancestor-keeping semantics. For search-as-you-type, debounce calls on the input side; each call is a single O(n) walk. It fires the change listeners.

type TreeSource

type TreeSource interface {
	// Root returns the id of the root node. widget.Tree treats the root itself
	// as invisible and shows its children as the top level.
	Root() string
	// Children returns the child ids of uid in source order.
	Children(uid string) []string
	// IsBranch reports whether uid may have children.
	IsBranch(uid string) bool
}

TreeSource is the read side of a hierarchical, string-keyed tree — the shape a widget.Tree consumes. Root returns the id of the (conventionally invisible) root node; Children returns the child ids of a node in source order; IsBranch reports whether a node may have children (so a tree can draw an expand affordance for it).

It is the tree-shaped analogue of Model for the flat List/Table layer. Where Model is a flat sequence, a TreeSource is a hierarchy, so TreeProxy filters and sorts it with tree semantics a flat proxy cannot express: ancestor-keeping filtering and per-sibling-group ordering.

Since: 1.0

func NewTreeSource

func NewTreeSource(root string, children func(uid string) []string, isBranch func(uid string) bool) TreeSource

NewTreeSource wraps a root id and the ChildUIDs/IsBranch callbacks a widget.Tree already uses into a TreeSource, so an existing tree can be put behind a TreeProxy without a new data structure. Nil callbacks are treated as "no children" / "not a branch".

Since: 1.0

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL