core/datatable/datatable.go
Functions
func A11yLabel
func A11yLabel(label string) Option {
return func(c *config) { c.a11yLabel = label }
}
func AccessibilityActions
AccessibilityActions returns the list of supported actions.
func (w *Widget) AccessibilityActions() []a11y.Action {
return []a11y.Action{a11y.ActionScrollUp, a11y.ActionScrollDown}
}
func AccessibilityHint
AccessibilityHint returns the accessibility hint.
func (w *Widget) AccessibilityHint() string {
return ""
}
func AccessibilityLabel
AccessibilityLabel returns the accessibility label.
func (w *Widget) AccessibilityLabel() string {
if w.cfg.a11yLabel != "" {
return w.cfg.a11yLabel
}
return defaultA11yLabel
}
func AccessibilityRole
AccessibilityRole returns the ARIA role for this widget.
func (w *Widget) AccessibilityRole() a11y.Role {
return a11y.RoleTable
}
func AccessibilityState
AccessibilityState returns the current accessibility state.
func (w *Widget) AccessibilityState() a11y.State {
return a11y.State{
Disabled: w.cfg.ResolvedDisabled(),
}
}
func AccessibilityValue
AccessibilityValue returns the current table state as a string.
func (w *Widget) AccessibilityValue() string {
count := w.cfg.ResolvedRowCount()
selected := w.cfg.ResolvedSelectedRow()
if selected >= 0 && selected < count {
return fmt.Sprintf("Row %d of %d selected", selected+1, count)
}
return fmt.Sprintf("%d rows", count)
}
func CellValue
CellValue sets the callback that provides cell text for a given row and column key.
func CellValue(fn func(row int, col string) string) Option {
return func(c *config) { c.cellValue = fn }
}
func Children
Children returns nil; rows are drawn directly, not as child widgets.
func (vc *virtualContent) Children() []widget.Widget {
return nil
}
func Children
Children returns the internal scroll view as the single child.
func (w *Widget) Children() []widget.Widget {
if w.scroll == nil {
return nil
}
return []widget.Widget{w.scroll}
}
func Columns
Columns sets the column definitions.
func Columns(cols []Column) Option {
return func(c *config) { c.columns = cols }
}
func Disabled
Disabled sets the table's disabled state.
func Disabled(d bool) Option {
return func(c *config) { c.disabled = d }
}
func DisabledFn
DisabledFn sets a dynamic function for the disabled state.
func DisabledFn(fn func() bool) Option {
return func(c *config) { c.disabledFn = fn }
}
func DisabledReadonlySignal
DisabledReadonlySignal binds the disabled state to a read-only signal.
func DisabledReadonlySignal(sig state.ReadonlySignal[bool]) Option {
return func(c *config) { c.readonlyDisabledSignal = sig }
}
func DisabledSignal
DisabledSignal binds the disabled state to a reactive signal.
func DisabledSignal(sig state.Signal[bool]) Option {
return func(c *config) { c.disabledSignal = sig }
}
func Draw
Draw renders the data table.
func (w *Widget) Draw(ctx widget.Context, canvas widget.Canvas) {
if !w.IsVisible() {
return
}
bounds := w.Bounds()
if bounds.IsEmpty() {
return
}
// Clip to table bounds.
canvas.PushClip(bounds)
defer canvas.PopClip()
rowCount := w.cfg.ResolvedRowCount()
if rowCount == 0 && len(w.cfg.columns) == 0 {
w.painter.PaintEmptyState(canvas, bounds)
return
}
// Draw header.
w.drawHeader(ctx, canvas, bounds)
// Draw data rows via the scroll view.
w.updateScrollBounds()
widget.StampScreenOrigin(w.scroll, canvas)
w.scroll.Draw(ctx, canvas)
}
func Draw
Draw renders only the visible data rows.
func (vc *virtualContent) Draw(ctx widget.Context, canvas widget.Canvas) {
if vc.table == nil {
return
}
vc.table.drawVisibleRows(ctx, canvas)
}
func Event
Event delegates events back to the parent table for row interaction.
func (vc *virtualContent) Event(ctx widget.Context, e event.Event) bool {
if vc.table == nil {
return false
}
me, ok := e.(*event.MouseEvent)
if !ok {
return false
}
return handleContentMouseEvent(vc.table, ctx, me)
}
func Event
Event handles an input event and returns true if consumed.
func (w *Widget) Event(ctx widget.Context, e event.Event) bool {
if !w.IsVisible() || !w.IsEnabled() {
return false
}
// Ensure scroll view bounds are set (they match the data area below the header).
// This is needed because ScrollView transforms event coordinates using its bounds,
// and bounds must be set before event dispatch, not just in Draw.
w.updateScrollBounds()
// Handle keyboard at table level.
if ke, ok := e.(*event.KeyEvent); ok {
if w.handleKeyEvent(ctx, ke) {
return true
}
}
// Handle header mouse events.
if me, ok := e.(*event.MouseEvent); ok {
if w.handleHeaderMouseEvent(ctx, me) {
return true
}
}
// Delegate to scroll view for data rows.
return w.scroll.Event(ctx, e)
}
func GetRowCount
GetRowCount returns the current row count.
func (w *Widget) GetRowCount() int {
return w.cfg.ResolvedRowCount()
}
func InvalidateData
InvalidateData signals that the underlying data has changed.
func (w *Widget) InvalidateData() {
// Nothing cached; just invalidate to trigger redraw.
}
func IsFocusable
IsFocusable reports whether the table can currently receive focus.
func (w *Widget) IsFocusable() bool {
return w.IsVisible() && w.IsEnabled() && !w.cfg.ResolvedDisabled()
}
func IsRowSelected
IsRowSelected reports whether the given row is selected.
func (w *Widget) IsRowSelected(row int) bool {
if w.cfg.selectionMode == SelectionMulti {
return w.cfg.selectedRows[row]
}
return row == w.cfg.ResolvedSelectedRow()
}
func Layout
Layout returns the total content size (all rows).
func (vc *virtualContent) Layout(_ widget.Context, c geometry.Constraints) geometry.Size {
if vc.table == nil {
return geometry.Size{}
}
totalHeight := vc.table.totalDataHeight()
width := c.MaxWidth
if width >= geometry.Infinity {
width = c.MinWidth
}
if width < c.MinWidth {
width = c.MinWidth
}
return geometry.Sz(width, totalHeight)
}
func Layout
Layout calculates the table's size within the given constraints.
func (w *Widget) Layout(ctx widget.Context, constraints geometry.Constraints) geometry.Size {
size := constraints.Biggest()
if size.Width >= geometry.Infinity {
size.Width = constraints.Constrain(geometry.Sz(defaultViewportWidth, 0)).Width
}
if size.Height >= geometry.Infinity {
totalH := defaultHeaderHeight + w.totalDataHeight()
if totalH > defaultViewportHeight {
totalH = defaultViewportHeight
}
size.Height = totalH
}
if size.Width <= 0 {
size.Width = defaultViewportWidth
}
if size.Height <= 0 {
size.Height = defaultViewportHeight
}
w.viewportWidth = size.Width
w.viewportHeight = size.Height
// Resolve column widths.
w.resolveColumnWidths()
// The scroll view occupies everything below the header.
scrollH := size.Height - defaultHeaderHeight
if scrollH < 0 {
scrollH = 0
}
svConstraints := geometry.Tight(geometry.Sz(size.Width, scrollH))
w.scroll.Layout(ctx, svConstraints)
return size
}
func Mount
Mount creates signal bindings for push-based invalidation.
func (w *Widget) Mount(ctx widget.Context) {
sched := ctx.Scheduler()
if sched == nil {
return
}
if w.cfg.readonlyRowCountSignal != nil {
b := state.BindToScheduler(w.cfg.readonlyRowCountSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.rowCountSignal != nil {
b := state.BindToScheduler(w.cfg.rowCountSignal, w, sched)
w.AddBinding(b)
}
if w.cfg.readonlySelectedRowSignal != nil {
b := state.BindToScheduler(w.cfg.readonlySelectedRowSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.selectedRowSignal != nil {
b := state.BindToScheduler(w.cfg.selectedRowSignal, w, sched)
w.AddBinding(b)
}
if w.cfg.readonlyDisabledSignal != nil {
b := state.BindToScheduler(w.cfg.readonlyDisabledSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.disabledSignal != nil {
b := state.BindToScheduler(w.cfg.disabledSignal, w, sched)
w.AddBinding(b)
}
w.scroll.Mount(ctx)
}
func New
New creates a new data table Widget with the given options.
The returned widget is visible, enabled, and focusable by default.
func New(opts ...Option) *Widget {
cfg := defaultConfig()
for _, opt := range opts {
opt(&cfg)
}
w := &Widget{
cfg: cfg,
painter: DefaultPainter{},
sortColumn: noSortColumn,
sortDirection: SortNone,
hoveredRow: noHoveredRow,
hoveredColHdr: noHoveredCol,
}
w.SetVisible(true)
w.SetEnabled(true)
if w.cfg.painter != nil {
w.painter = w.cfg.painter
}
// Initialize multi-selection map.
if w.cfg.selectionMode == SelectionMulti && w.cfg.selectedRows == nil {
w.cfg.selectedRows = make(map[int]bool)
}
// Create virtual content widget.
w.virtual = &virtualContent{table: w}
w.virtual.SetVisible(true)
// Build internal scroll view.
svOpts := []scrollview.Option{
scrollview.DirectionOpt(scrollview.Vertical),
}
if w.cfg.scrollYSignal != nil {
svOpts = append(svOpts, scrollview.ScrollYSignal(w.cfg.scrollYSignal))
}
if w.cfg.onScroll != nil {
fn := w.cfg.onScroll
svOpts = append(svOpts, scrollview.OnScroll(func(_, y float32) {
fn(y)
}))
}
w.scroll = scrollview.New(w.virtual, svOpts...)
// ADR-028: parent chain for upward dirty propagation.
// Flutter: RenderObject.adoptChild sets parent on each child.
w.scroll.SetParent(w)
return w
}
func OnRowSelect
OnRowSelect sets the callback invoked when a row is selected.
func OnRowSelect(fn func(row int)) Option {
return func(c *config) { c.onRowSelect = fn }
}
func OnScroll
OnScroll sets the callback invoked when the scroll position changes.
func OnScroll(fn func(offset float32)) Option {
return func(c *config) { c.onScroll = fn }
}
func OnSort
OnSort sets the callback invoked when a column sort changes.
The ascending parameter indicates the new sort direction (true=asc, false=desc).
The callback is NOT invoked when sort is cleared (direction returns to None).
func OnSort(fn func(col string, ascending bool)) Option {
return func(c *config) { c.onSort = fn }
}
func PainterOpt
PainterOpt sets the painter used to render table visuals.
func PainterOpt(p Painter) Option {
return func(c *config) { c.painter = p }
}
func ResolvedDisabled
ResolvedDisabled returns the current disabled state.
Priority: ReadonlySignal > Signal > Fn > Static.
func (c *config) ResolvedDisabled() bool {
if c.readonlyDisabledSignal != nil {
return c.readonlyDisabledSignal.Get()
}
if c.disabledSignal != nil {
return c.disabledSignal.Get()
}
if c.disabledFn != nil {
return c.disabledFn()
}
return c.disabled
}
func ResolvedRowCount
ResolvedRowCount returns the current row count.
Priority: ReadonlySignal > Signal > Fn > Static.
func (c *config) ResolvedRowCount() int {
if c.readonlyRowCountSignal != nil {
return c.readonlyRowCountSignal.Get()
}
if c.rowCountSignal != nil {
return c.rowCountSignal.Get()
}
if c.rowCountFn != nil {
return c.rowCountFn()
}
return c.rowCount
}
func ResolvedSelectedRow
ResolvedSelectedRow returns the current selected row index.
Priority: ReadonlySignal > Signal > Static. Returns -1 if no selection.
func (c *config) ResolvedSelectedRow() int {
if c.readonlySelectedRowSignal != nil {
return c.readonlySelectedRowSignal.Get()
}
if c.selectedRowSignal != nil {
return c.selectedRowSignal.Get()
}
return c.selectedRow
}
func RowCount
RowCount sets the total number of data rows.
func RowCount(n int) Option {
return func(c *config) { c.rowCount = n }
}
func RowCountFn
RowCountFn sets a dynamic function that returns the row count.
func RowCountFn(fn func() int) Option {
return func(c *config) { c.rowCountFn = fn }
}
func RowCountReadonlySignal
RowCountReadonlySignal binds the row count to a read-only signal.
func RowCountReadonlySignal(sig state.ReadonlySignal[int]) Option {
return func(c *config) { c.readonlyRowCountSignal = sig }
}
func RowCountSignal
RowCountSignal binds the row count to a reactive signal.
func RowCountSignal(sig state.Signal[int]) Option {
return func(c *config) { c.rowCountSignal = sig }
}
func RowHeight
RowHeight sets the height of each data row in logical pixels. Default: 32.
func RowHeight(h float32) Option {
return func(c *config) { c.rowHeight = h }
}
func ScrollToRow
ScrollToRow scrolls to make the given row visible.
func (w *Widget) ScrollToRow(row int) {
rowCount := w.cfg.ResolvedRowCount()
if row < 0 || row >= rowCount {
return
}
rowTop := float32(row) * w.cfg.rowHeight
rowBottom := rowTop + w.cfg.rowHeight
scrollY := w.currentScrollY()
dataViewH := w.viewportHeight - defaultHeaderHeight
if dataViewH <= 0 {
return
}
if rowTop >= scrollY && rowBottom <= scrollY+dataViewH {
return // already visible
}
var newY float32
if rowTop < scrollY {
newY = rowTop
} else {
newY = rowBottom - dataViewH
if newY < 0 {
newY = 0
}
}
w.setScrollY(newY)
}
func ScrollYSignal
ScrollYSignal binds the vertical scroll offset to a reactive signal (TWO-WAY).
func ScrollYSignal(sig state.Signal[float32]) Option {
return func(c *config) { c.scrollYSignal = sig }
}
func SelectedRow
SelectedRow sets the initially selected row index. Use -1 for no selection.
func SelectedRow(row int) Option {
return func(c *config) { c.selectedRow = row }
}
func SelectedRowReadonlySignal
SelectedRowReadonlySignal binds the selected row to a read-only signal.
func SelectedRowReadonlySignal(sig state.ReadonlySignal[int]) Option {
return func(c *config) { c.readonlySelectedRowSignal = sig }
}
func SelectedRowSignal
SelectedRowSignal binds the selected row to a reactive signal.
TWO-WAY binding: reads from signal, writes back on user selection.
func SelectedRowSignal(sig state.Signal[int]) Option {
return func(c *config) { c.selectedRowSignal = sig }
}
func SelectionModeOpt
SelectionModeOpt sets the row selection mode. Default: SelectionNone.
func SelectionModeOpt(mode SelectionMode) Option {
return func(c *config) { c.selectionMode = mode }
}
func SetSort
SetSort programmatically sets the sort column and direction.
Pass an empty key or SortNone to clear sorting.
func (w *Widget) SetSort(colKey string, dir SortDirection) {
if colKey == "" || dir == SortNone {
w.sortColumn = noSortColumn
w.sortDirection = SortNone
return
}
for i, col := range w.cfg.columns {
if col.Key == colKey {
w.sortColumn = i
w.sortDirection = dir
return
}
}
}
func SortColumn
SortColumn returns the currently sorted column key and direction.
Returns empty string and SortNone if no column is sorted.
func (w *Widget) SortColumn() (string, SortDirection) {
if w.sortColumn < 0 || w.sortColumn >= len(w.cfg.columns) {
return "", SortNone
}
return w.cfg.columns[w.sortColumn].Key, w.sortDirection
}
func String
String returns a human-readable name for the selection mode.
func (m SelectionMode) String() string {
switch m {
case SelectionNone:
return selModeNoneStr
case SelectionSingle:
return selModeSingleStr
case SelectionMulti:
return selModeMultiStr
default:
return selModeUnknownStr
}
}
func Unmount
Unmount is called when the table is removed from the widget tree.
func (w *Widget) Unmount() {
w.scroll.Unmount()
}
func VisibleRowRange
VisibleRowRange returns the indices of currently visible rows [start, end).
func (w *Widget) VisibleRowRange() (start, end int) {
rowCount := w.cfg.ResolvedRowCount()
if rowCount == 0 {
return 0, 0
}
scrollY := w.currentScrollY()
dataViewH := w.viewportHeight - defaultHeaderHeight
if dataViewH <= 0 {
return 0, 0
}
first, last := w.visibleRowRange(scrollY, dataViewH)
if last >= rowCount {
last = rowCount - 1
}
if first > last {
first = last
}
return first, last + 1
}
Structs
type Widget struct
Widget implements a sortable data table with fixed header, virtualized rows,
and pluggable painting.
A data table is created with [New] using functional options:
dt := datatable.New(
datatable.Columns(cols),
datatable.RowCount(1000),
datatable.CellValue(valueFn),
)
type Widget struct {
widget.WidgetBase
cfg config
painter Painter
// Internal scroll view for the data rows (not header).
scroll *scrollview.Widget
virtual *virtualContent
// Layout state.
viewportWidth float32
viewportHeight float32
colWidths []float32 // resolved column widths
// Sort state.
sortColumn int // index into cfg.columns, -1 = none
sortDirection SortDirection // current sort direction
// Interaction state.
hoveredRow int // data row under cursor, -1 = none
hoveredColHdr int // header column under cursor, -1 = none
}
A11yLabel sets the accessibility label for the table.