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 ¶
- func StringLess(a, b string) bool
- type FilterProxy
- func (s *FilterProxy) AddListener(l binding.DataListener)
- func (p *FilterProxy[T]) Close()
- func (p *FilterProxy[T]) Item(i int) T
- func (p *FilterProxy[T]) Len() int
- func (s *FilterProxy) RemoveListener(l binding.DataListener)
- func (p *FilterProxy[T]) SetItem(i int, item T)
- func (p *FilterProxy[T]) SetPredicate(keep func(T) bool)
- func (p *FilterProxy[T]) SourceIndex(i int) int
- type Model
- type MutableModel
- type SelectionMode
- type SelectionModel
- func (s *SelectionModel) AddListener(l binding.DataListener)
- func (s *SelectionModel[T, K]) Clear()
- func (s *SelectionModel[T, K]) IndexOf(k K) int
- func (s *SelectionModel[T, K]) IsIndexSelected(i int) bool
- func (s *SelectionModel[T, K]) IsSelected(k K) bool
- func (s *SelectionModel[T, K]) Len() int
- func (s *SelectionModel) RemoveListener(l binding.DataListener)
- func (s *SelectionModel[T, K]) Select(k K)
- func (s *SelectionModel[T, K]) SelectIndex(i int)
- func (s *SelectionModel[T, K]) SelectRange(to int)
- func (s *SelectionModel[T, K]) Selected() []K
- func (s *SelectionModel[T, K]) SelectedIndices() []int
- func (s *SelectionModel[T, K]) SetMode(mode SelectionMode)
- func (s *SelectionModel[T, K]) Toggle(k K)
- func (s *SelectionModel[T, K]) ToggleIndex(i int)
- func (s *SelectionModel[T, K]) Unselect(k K)
- type SliceModel
- func (s *SliceModel) AddListener(l binding.DataListener)
- func (m *SliceModel[T]) Append(items ...T)
- func (m *SliceModel[T]) Item(i int) T
- func (m *SliceModel[T]) Len() int
- func (s *SliceModel) RemoveListener(l binding.DataListener)
- func (m *SliceModel[T]) Set(items []T)
- func (m *SliceModel[T]) SetItem(i int, item T)
- type SortProxy
- func (s *SortProxy) AddListener(l binding.DataListener)
- func (p *SortProxy[T]) Close()
- func (p *SortProxy[T]) Item(i int) T
- func (p *SortProxy[T]) Len() int
- func (s *SortProxy) RemoveListener(l binding.DataListener)
- func (p *SortProxy[T]) SetItem(i int, item T)
- func (p *SortProxy[T]) SetLess(less func(a, b T) bool)
- func (p *SortProxy[T]) SourceIndex(i int) int
- type TreeProxy
- func (s *TreeProxy) AddListener(l binding.DataListener)
- func (p *TreeProxy) Children(uid string) []string
- func (p *TreeProxy) IsBranch(uid string) bool
- func (p *TreeProxy) Refresh()
- func (s *TreeProxy) RemoveListener(l binding.DataListener)
- func (p *TreeProxy) Root() string
- func (p *TreeProxy) SetKeepMatchingSubtree(keep bool)
- func (p *TreeProxy) SetLess(less func(a, b string) bool)
- func (p *TreeProxy) SetPredicate(keep func(uid string) bool)
- type TreeSource
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func StringLess ¶
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 ¶
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) 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 ¶
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) RemoveListener ¶
func (s *SortProxy) RemoveListener(l binding.DataListener)
RemoveListener detaches the first registration matching l.
func (*SortProxy[T]) SetItem ¶
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 ¶
SetLess replaces the comparator (nil restores source order) and re-sorts.
func (*SortProxy[T]) SourceIndex ¶
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 ¶
Children returns the visible, sorted child ids of uid. The returned slice is a copy the caller may retain or mutate.
func (*TreeProxy) IsBranch ¶
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) SetKeepMatchingSubtree ¶
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 ¶
SetLess replaces the per-sibling-group comparator (nil restores source order) and re-sorts. It fires the change listeners.
func (*TreeProxy) SetPredicate ¶
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