core/gridview/gridview.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 "Grid"
}
func AccessibilityRole
AccessibilityRole returns the ARIA role for this widget.
func (w *Widget) AccessibilityRole() a11y.Role {
return a11y.RoleGrid
}
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 grid state as a string.
func (w *Widget) AccessibilityValue() string {
count := w.cfg.ResolvedItemCount()
selected := w.cfg.ResolvedSelectedIndex()
if selected >= 0 && selected < count {
return fmt.Sprintf("Item %d of %d selected", selected+1, count)
}
return fmt.Sprintf("%d items", count)
}
func BuildCell
BuildCell sets the callback that creates a widget for each visible cell.
The callback receives the item index and a [CellContext] with the cell's state.
This is the primary convenience API for providing grid content.
Internally, the function is wrapped into a [cdk.FuncContent] so that the
grid view uniformly operates on [cdk.Content] for cell rendering.
func BuildCell(fn func(index int, ctx CellContext) widget.Widget) Option {
return func(c *config) {
c.cellContent = cdk.FuncContent[CellContext]{Fn: func(ctx CellContext) widget.Widget {
return fn(ctx.Index, ctx)
}}
}
}
func CellContent
CellContent sets the [cdk.Content] used to render each visible cell.
This is the enterprise API for providing grid content -- use it when you
need reusable content implementations or progressive complexity.
For most use cases, [BuildCell] is simpler and sufficient.
func CellContent(content cdk.Content[CellContext]) Option {
return func(c *config) { c.cellContent = content }
}
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 Children
Children returns nil; visible cell widgets are ephemeral and managed by the cache.
func (vc *virtualContent) Children() []widget.Widget {
return nil
}
func Columns
Columns sets the fixed number of columns in the grid.
Default: 4. See also [ColumnsAuto] for auto-fit mode.
func Columns(n int) Option {
return func(c *config) {
c.columns = n
c.columnsAuto = false
}
}
func ColumnsAuto
ColumnsAuto enables auto-fit column count based on viewport width.
When enabled, the column count is calculated as floor(viewportWidth / (itemWidth + gap)).
func ColumnsAuto(enabled bool) Option {
return func(c *config) { c.columnsAuto = enabled }
}
func ColumnsReadonlySignal
ColumnsReadonlySignal binds the column count to a read-only signal.
When set, this takes highest precedence. Ignored if [ColumnsAuto] is enabled.
func ColumnsReadonlySignal(sig state.ReadonlySignal[int]) Option {
return func(c *config) { c.readonlyColumnsSignal = sig }
}
func ColumnsSignal
ColumnsSignal binds the column count to a reactive signal.
When set, the signal takes precedence over [Columns] but not over
[ColumnsReadonlySignal]. Ignored if [ColumnsAuto] is enabled.
func ColumnsSignal(sig state.Signal[int]) Option {
return func(c *config) { c.columnsSignal = sig }
}
func Disabled
Disabled sets the grid view'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.
When set, this takes precedence over [Disabled] but not over [DisabledSignal].
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.
When set, this takes highest precedence over all other disabled sources.
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.
When set, the signal takes precedence over [DisabledFn] and [Disabled]
but not over [DisabledReadonlySignal].
func DisabledSignal(sig state.Signal[bool]) Option {
return func(c *config) { c.disabledSignal = sig }
}
func Draw
Draw renders only the visible cells within the current viewport.
func (vc *virtualContent) Draw(ctx widget.Context, canvas widget.Canvas) {
if vc.grid == nil {
return
}
vc.grid.drawVisibleCells(ctx, canvas)
}
func Draw
Draw renders the grid view to the canvas.
func (w *Widget) Draw(ctx widget.Context, canvas widget.Canvas) {
if !w.IsVisible() {
return
}
bounds := w.Bounds()
if bounds.IsEmpty() {
return
}
// Set scroll view bounds to match our bounds.
w.scroll.SetBounds(bounds)
// Stamp screen origin on the internal scroll view so its ScreenBounds()
// returns correct window-space coordinates for dirty region collection.
widget.StampScreenOrigin(w.scroll, canvas)
// Delegate drawing to the internal scroll view.
w.scroll.Draw(ctx, canvas)
}
func Event
Event delegates events back to the parent grid for cell interaction.
func (vc *virtualContent) Event(ctx widget.Context, e event.Event) bool {
if vc.grid == nil {
return false
}
return handleContentEvent(vc.grid, ctx, e)
}
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
}
// Handle keyboard events at the grid level.
if ke, ok := e.(*event.KeyEvent); ok {
if w.handleKeyEvent(ctx, ke) {
return true
}
}
// Delegate other events to the scroll view.
return w.scroll.Event(ctx, e)
}
func Gap
Gap sets the spacing between cells in pixels. Default: 0.
func Gap(g float32) Option {
return func(c *config) { c.gap = g }
}
func GetColumns
GetColumns returns the current effective column count.
func (w *Widget) GetColumns() int {
return w.effectiveColumns()
}
func GetItemCount
GetItemCount returns the current item count.
func (w *Widget) GetItemCount() int {
return w.cfg.ResolvedItemCount()
}
func InvalidateData
InvalidateData signals that the underlying data has changed.
This invalidates the widget cache and triggers re-layout.
func (w *Widget) InvalidateData() {
w.cache.invalidate()
}
func IsFocusable
IsFocusable reports whether the grid view can currently receive focus.
func (w *Widget) IsFocusable() bool {
return w.IsVisible() && w.IsEnabled() && !w.cfg.ResolvedDisabled()
}
func ItemCount
ItemCount sets the total number of items in the grid.
func ItemCount(n int) Option {
return func(c *config) { c.itemCount = n }
}
func ItemCountFn
ItemCountFn sets a dynamic function that returns the item count.
When set, this takes precedence over [ItemCount] but not over [ItemCountSignal].
func ItemCountFn(fn func() int) Option {
return func(c *config) { c.itemCountFn = fn }
}
func ItemCountReadonlySignal
ItemCountReadonlySignal binds the item count to a read-only signal.
When set, this takes highest precedence over all other item count sources.
func ItemCountReadonlySignal(sig state.ReadonlySignal[int]) Option {
return func(c *config) { c.readonlyItemCountSignal = sig }
}
func ItemCountSignal
ItemCountSignal binds the item count to a reactive signal.
When set, the signal takes precedence over [ItemCountFn] and [ItemCount]
but not over [ItemCountReadonlySignal].
func ItemCountSignal(sig state.Signal[int]) Option {
return func(c *config) { c.itemCountSignal = sig }
}
func ItemSize
ItemSize sets the fixed width and height for all grid cells.
Default: 120x120.
func ItemSize(width, height float32) Option {
return func(c *config) {
c.itemWidth = width
c.itemHeight = height
}
}
func Layout
Layout returns the total content size.
func (vc *virtualContent) Layout(_ widget.Context, c geometry.Constraints) geometry.Size {
if vc.grid == nil {
return geometry.Size{}
}
totalHeight := vc.grid.totalContentHeight()
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 grid view'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 := w.totalContentHeight()
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
// Compute effective column count.
w.effectiveCols = w.computeEffectiveColumns()
// Layout the internal scroll view with concrete constraints.
svConstraints := geometry.Tight(size)
w.scroll.Layout(ctx, svConstraints)
return size
}
func Mount
Mount creates signal bindings for push-based invalidation.
Implements [widget.Lifecycle].
func (w *Widget) Mount(ctx widget.Context) {
sched := ctx.Scheduler()
if sched == nil {
return
}
// Bind item count signals.
if w.cfg.readonlyItemCountSignal != nil {
b := state.BindToScheduler(w.cfg.readonlyItemCountSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.itemCountSignal != nil {
b := state.BindToScheduler(w.cfg.itemCountSignal, w, sched)
w.AddBinding(b)
}
// Bind selected index signals.
if w.cfg.readonlySelectedIndexSignal != nil {
b := state.BindToScheduler(w.cfg.readonlySelectedIndexSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.selectedIndexSignal != nil {
b := state.BindToScheduler(w.cfg.selectedIndexSignal, w, sched)
w.AddBinding(b)
}
// Bind disabled signals.
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)
}
// Bind columns signals.
if w.cfg.readonlyColumnsSignal != nil {
b := state.BindToScheduler(w.cfg.readonlyColumnsSignal, w, sched)
w.AddBinding(b)
} else if w.cfg.columnsSignal != nil {
b := state.BindToScheduler(w.cfg.columnsSignal, w, sched)
w.AddBinding(b)
}
// Mount internal scroll view.
w.scroll.Mount(ctx)
}
func New
New creates a new grid view Widget with the given options.
The returned widget is visible, enabled, and focusable by default.
If no [BuildCell] callback is provided, the grid renders empty.
func New(opts ...Option) *Widget {
cfg := defaultConfig()
for _, opt := range opts {
opt(&cfg)
}
w := &Widget{
cfg: cfg,
painter: DefaultPainter{},
hoveredIndex: noHoveredIndex,
}
w.SetVisible(true)
w.SetEnabled(true)
if w.cfg.painter != nil {
w.painter = w.cfg.painter
}
// Create virtual content widget.
w.virtual = &virtualContent{grid: w}
w.virtual.SetVisible(true)
// Build internal scroll view options.
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.cache.invalidate()
}))
} else {
svOpts = append(svOpts, scrollview.OnScroll(func(_, _ float32) {
w.cache.invalidate()
}))
}
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 OnCellClick
OnCellClick sets the callback invoked when a cell is clicked.
func OnCellClick(fn func(index int)) Option {
return func(c *config) { c.onCellClick = 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 OnSelectionChange
OnSelectionChange sets the callback invoked when the selected cell changes.
func OnSelectionChange(fn func(index int)) Option {
return func(c *config) { c.onSelectionChange = fn }
}
func PainterOpt
PainterOpt sets the painter used to render grid-specific visuals.
Each design system provides its own painter. If not set,
[DefaultPainter] is used.
func PainterOpt(p Painter) Option {
return func(c *config) { c.painter = p }
}
func ResolvedColumns
ResolvedColumns returns the current column count.
Priority: ReadonlySignal > Signal > Static.
func (c *config) ResolvedColumns() int {
if c.readonlyColumnsSignal != nil {
return c.readonlyColumnsSignal.Get()
}
if c.columnsSignal != nil {
return c.columnsSignal.Get()
}
return c.columns
}
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 ResolvedItemCount
ResolvedItemCount returns the current item count.
Priority: ReadonlySignal > Signal > Fn > Static.
func (c *config) ResolvedItemCount() int {
if c.readonlyItemCountSignal != nil {
return c.readonlyItemCountSignal.Get()
}
if c.itemCountSignal != nil {
return c.itemCountSignal.Get()
}
if c.itemCountFn != nil {
return c.itemCountFn()
}
return c.itemCount
}
func ResolvedSelectedIndex
ResolvedSelectedIndex returns the current selected index.
Priority: ReadonlySignal > Signal > Static.
Returns -1 if no selection.
func (c *config) ResolvedSelectedIndex() int {
if c.readonlySelectedIndexSignal != nil {
return c.readonlySelectedIndexSignal.Get()
}
if c.selectedIndexSignal != nil {
return c.selectedIndexSignal.Get()
}
return c.selectedIndex
}
func ScrollToIndex
ScrollToIndex scrolls to make the cell at the given index visible.
If the cell is already fully visible, this is a no-op.
func (w *Widget) ScrollToIndex(index int) {
itemCount := w.cfg.ResolvedItemCount()
if index < 0 || index >= itemCount {
return
}
cols := w.effectiveColumns()
if cols <= 0 {
return
}
row := index / cols
cellTop := float32(row) * (w.cfg.itemHeight + w.cfg.gap)
cellBottom := cellTop + w.cfg.itemHeight
scrollY := w.currentScrollY()
// Already visible: no-op.
if cellTop >= scrollY && cellBottom <= scrollY+w.viewportHeight {
return
}
var newScrollY float32
if cellTop < scrollY {
newScrollY = cellTop
} else {
newScrollY = cellBottom - w.viewportHeight
if newScrollY < 0 {
newScrollY = 0
}
}
w.setScrollY(newScrollY)
}
func ScrollYSignal
ScrollYSignal binds the vertical scroll offset to a reactive signal.
This is a TWO-WAY binding passed through to the internal ScrollView.
func ScrollYSignal(sig state.Signal[float32]) Option {
return func(c *config) { c.scrollYSignal = sig }
}
func SelectedIndex
SelectedIndex sets the initially selected cell index.
Use -1 for no selection.
func SelectedIndex(index int) Option {
return func(c *config) { c.selectedIndex = index }
}
func SelectedIndexReadonlySignal
SelectedIndexReadonlySignal binds the selected index to a read-only signal.
When set, this takes highest precedence over all other selection sources.
func SelectedIndexReadonlySignal(sig state.ReadonlySignal[int]) Option {
return func(c *config) { c.readonlySelectedIndexSignal = sig }
}
func SelectedIndexSignal
SelectedIndexSignal binds the selected index to a reactive signal.
This is a TWO-WAY binding: the widget reads from the signal, and
when the user selects a cell, the new index is written back.
func SelectedIndexSignal(sig state.Signal[int]) Option {
return func(c *config) { c.selectedIndexSignal = sig }
}
func SelectionModeOpt
SelectionModeOpt sets the selection mode for the grid.
Default is [SelectionNone].
func SelectionModeOpt(mode SelectionMode) Option {
return func(c *config) { c.selectionMode = mode }
}
func String
String returns a human-readable name for the selection mode.
func (m SelectionMode) String() string {
switch m {
case SelectionNone:
return selectionNoneStr
case SelectionSingle:
return selectionSingleStr
default:
return selectionUnknownStr
}
}
func Unmount
Unmount is called when the grid view is removed from the widget tree.
Implements [widget.Lifecycle].
func (w *Widget) Unmount() {
w.scroll.Unmount()
// Bindings are cleaned up automatically by WidgetBase.CleanupBindings().
}
func VisibleRange
VisibleRange returns the indices of currently visible cells [start, end).
func (w *Widget) VisibleRange() (start, end int) {
cols := w.effectiveColumns()
if cols <= 0 {
return 0, 0
}
itemCount := w.cfg.ResolvedItemCount()
if itemCount == 0 {
return 0, 0
}
scrollY := w.currentScrollY()
firstRow, lastRow := w.visibleRowRange(scrollY, w.viewportHeight)
startIdx := firstRow * cols
endIdx := (lastRow + 1) * cols
if endIdx > itemCount {
endIdx = itemCount
}
if startIdx > itemCount {
startIdx = itemCount
}
return startIdx, endIdx
}
Structs
type CellContext struct
CellContext provides contextual information to the cell builder callback.
The builder receives a CellContext for each visible cell, allowing it to
customize the returned widget based on selection, hover, and position state.
type CellContext struct {
// Index is the zero-based item index in the data source.
Index int
// Row is the cell's row index (zero-based).
Row int
// Col is the cell's column index (zero-based).
Col int
// IsSelected is true if this cell is the currently selected cell.
IsSelected bool
// IsHovered is true if the mouse cursor is over this cell.
IsHovered bool
}
type Widget struct
Widget implements a virtualized grid that renders only visible cells.
It composes an internal [scrollview.Widget] for scroll handling and
delegates cell rendering to a builder callback.
A grid view is created with [New] using functional options:
gv := gridview.New(
gridview.Columns(4),
gridview.ItemCount(1000),
gridview.ItemSize(120, 120),
gridview.BuildCell(func(index int, ctx gridview.CellContext) widget.Widget {
return buildCell(index, ctx.IsSelected)
}),
)
type Widget struct {
widget.WidgetBase
cfg config
painter Painter
// Internal scroll view (composition).
scroll *scrollview.Widget
virtual *virtualContent
// Widget cache for visible cells.
cache cellCache
// Layout state.
viewportWidth float32
viewportHeight float32
effectiveCols int // actual columns after auto-fit
// Interaction state.
hoveredIndex int
}
A11yLabel sets the accessibility label for the grid.