core/datatable/datatable.go

Functions Structs

Functions

func A11yLabel

A11yLabel sets the accessibility label for the table.

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
}