Skip to main content

core

Package core provides the widget and element framework interfaces and lifecycle.

This package defines the foundational types for building reactive user interfaces: Widget, Element, State, and BuildContext. It follows a declarative UI model where widgets describe what the UI should look like, and the framework efficiently updates the actual UI to match.

Core Types

Widget is an immutable description of part of the UI. Widgets are lightweight configuration objects that can be created frequently without performance concerns.

Element is the instantiation of a Widget at a particular location in the tree. Elements manage the lifecycle and identity of widgets.

Stateful Widgets

For widgets that need mutable state, embed StateBase in your state struct:

type myState struct {
core.StateBase
count int
}

func (s *myState) InitState() {
// Initialize state here
}

func (s *myState) Build(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: fmt.Sprintf("Count: %d", s.count)}
}

State Management

Signal provides thread-safe reactive values with automatic rebuild:

s.counter = core.NewSignal(0)
core.UseListenable(s, s.counter) // Subscribe for rebuilds
s.counter.Set(5) // Triggers rebuild

Hooks

UseDisposable and UseListenable help manage resources and subscriptions with automatic cleanup on disposal.

Constructor Conventions

Controllers and services use NewX() constructors returning pointers:

ctrl := animation.NewAnimationController(time.Second)
channel := platform.NewMethodChannel("app.channel")

This distinguishes long-lived, mutable objects (controllers) from immutable configuration objects (widgets, which use struct literals or XxxOf() helpers).

Package core provides the core widget and element framework.

Variables

DebugMode controls whether debug information is displayed in error widgets. When true, error widgets show detailed error messages and stack traces. When false, error widgets show minimal information.

var DebugMode = true

func GlobalOffsetOf

func GlobalOffsetOf(element Element) graphics.Offset

GlobalOffsetOf returns the accumulated offset for an element in the render tree.

func MustProvide

func MustProvide[T any](ctx BuildContext) T

MustProvide finds and depends on the nearest ancestor InheritedProvider[T]. Panics if not found in the ancestor chain.

Example:

user := core.MustProvide[*User](ctx)
fmt.Println("Hello,", user.Name)

func Provide

func Provide[T any](ctx BuildContext) (T, bool)

Provide finds and depends on the nearest ancestor InheritedProvider[T]. Returns the value and true if found, or the zero value and false if not found.

Example:

if user, ok := core.Provide[*User](ctx); ok {
fmt.Println("Hello,", user.Name)
}

func SetDebugMode

func SetDebugMode(debug bool)

SetDebugMode enables or disables debug mode for the framework.

func SetErrorWidgetBuilder

func SetErrorWidgetBuilder(builder ErrorWidgetBuilder)

SetErrorWidgetBuilder configures the global error widget builder. Pass nil to restore the default builder.

func UseDisposable

func UseDisposable(s stateBase, d Disposable)

UseDisposable registers a Disposable resource for automatic cleanup when the state is disposed. Call this once in InitState, not in Build.

Example:

func (s *myState) InitState() {
s.animation = animation.NewAnimationController(300 * time.Millisecond)
core.UseDisposable(s, s.animation)
}

func UseListenable

func UseListenable(s stateBase, listenable Listenable)

UseListenable subscribes to a Listenable and triggers rebuilds. The subscription is automatically cleaned up when the state is disposed.

Works with Signal, Derived, Notifier, and any custom type that implements Listenable.

The listener callback calls StateBase.SetState, which is not thread-safe. If the source is mutated from background goroutines, wrap the mutation with drift.Dispatch to ensure notifications arrive on the UI thread.

Example:

func (s *myState) InitState() {
s.counter = core.NewSignal(0)
core.UseListenable(s, s.counter)
}

func (s *myState) Build(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: fmt.Sprintf("Count: %d", s.counter.Value())}
}

func UseSelector

func UseSelector[S comparable](st stateBase, source Listenable, selector func() S)

UseSelector subscribes to a Listenable source but only triggers rebuilds when the selector returns a different value. The selector extracts the relevant portion of state; the widget only rebuilds when that portion changes.

S must be comparable. For non-comparable selected types (slices, maps), use UseSelectorWithEquality instead.

This is useful when a signal holds a large struct but the widget only depends on one field. Without a selector, every notification would trigger a rebuild.

Call this once in InitState, not in Build. The subscription is automatically cleaned up when the state is disposed.

Like other hooks, the listener callback calls SetState, which is not thread-safe. If the source is mutated from background goroutines, wrap the Set call with drift.Dispatch to ensure notifications arrive on the UI thread.

Example:

func (s *myState) InitState() {
// Only rebuilds when user.Name changes, ignoring other field updates
core.UseSelector(s, s.user, func() string {
return s.user.Value().Name
})
}

func UseSelectorWithEquality

func UseSelectorWithEquality[S any](st stateBase, source Listenable, selector func() S, equal func(a, b S) bool)

UseSelectorWithEquality is like UseSelector but accepts a custom equality function for comparing selected values. Use this when the selected type is non-comparable (slices, maps) or when you need semantic equality that differs from Go's == operator:

core.UseSelectorWithEquality(s, s.store, func() []string {
return s.store.Value().Tags
}, slices.Equal)

type AspectAwareInheritedWidget

AspectAwareInheritedWidget is implemented by inherited widgets that support granular per-dependent rebuild filtering based on registered aspects. Most inherited widgets do not need this; the default behavior notifies all dependents when [InheritedWidget.ShouldRebuildDependents] returns true.

When implemented, [ShouldRebuildDependent] is called for each dependent that registered specific aspects via [BuildContext.DependOnInherited]. Dependents that registered with a nil aspect (depending on all changes) are always notified.

type AspectAwareInheritedWidget interface {
InheritedWidget
// ShouldRebuildDependent returns true if a specific dependent should
// rebuild based on the aspects it registered. The aspects map contains all
// aspects the dependent registered via [BuildContext.DependOnInherited].
ShouldRebuildDependent(oldWidget InheritedWidget, aspects map[any]struct{}) bool
}

type BuildContext

BuildContext provides access to the widget tree during the build phase.

BuildContext is passed to [StatelessWidget.Build] and [State.Build] methods, providing access to inherited data and ancestor widgets.

The most common use is accessing InheritedWidget data:

func (w MyWidget) Build(ctx core.BuildContext) core.Widget {
theme := theme.ThemeOf(ctx) // Uses DependOnInherited internally
return Text{Content: "Hello", Style: theme.TextTheme.BodyLarge}
}
type BuildContext interface {
// Widget returns the widget that created this context.
Widget() Widget
// FindAncestor walks up the tree and returns the first element matching
// the predicate, or nil if none is found.
FindAncestor(predicate func(Element) bool) Element
// DependOnInherited finds the nearest ancestor [InheritedWidget] of the given
// type and registers this context as a dependent. When the inherited widget
// updates and notifies dependents, this widget will rebuild.
//
// The aspect parameter enables granular dependency tracking: when non-nil,
// only changes affecting that aspect trigger rebuilds. Pass nil to rebuild
// on any change. Use [reflect.TypeOf] to get the inherited widget's type:
//
// themeType := reflect.TypeOf(MyTheme{})
// widget := ctx.DependOnInherited(themeType, "colors")
//
// For simpler access, use [InheritedProvider] with [Provide].
DependOnInherited(inheritedType reflect.Type, aspect any) any
// DependOnInheritedWithAspects is like DependOnInherited but registers multiple
// aspects in a single tree walk. More efficient when depending on several aspects
// of the same inherited widget.
DependOnInheritedWithAspects(inheritedType reflect.Type, aspects ...any) any
}

type BuildOwner

BuildOwner tracks dirty elements that need rebuilding.

type BuildOwner struct {

// OnNeedsFrame is called when a new element is scheduled for rebuild,
// signalling the platform that a frame should be rendered. This is
// necessary for on-demand frame scheduling where the display link is
// paused until explicitly requested.
OnNeedsFrame func()
// contains filtered or unexported fields
}

func NewBuildOwner

func NewBuildOwner() *BuildOwner

NewBuildOwner creates a new BuildOwner.

func (*BuildOwner) FlushBuild

func (b *BuildOwner) FlushBuild()

FlushBuild rebuilds all dirty elements in depth order.

func (*BuildOwner) NeedsWork

func (b *BuildOwner) NeedsWork() bool

NeedsWork returns true if there are dirty elements or pending layout/paint.

func (*BuildOwner) Pipeline

func (b *BuildOwner) Pipeline() *layout.PipelineOwner

Pipeline returns the PipelineOwner for render object scheduling.

func (*BuildOwner) RegisterGlobalKey

func (b *BuildOwner) RegisterGlobalKey(key any, element Element)

RegisterGlobalKey associates a global key identity with an element in the owner's registry. This is called automatically by the framework when an element whose widget returns a GlobalKey is mounted. If a key is already registered, the new element silently replaces the previous entry.

The key parameter is the inner identity pointer obtained from globalKeyRegistry.globalKeyImpl(), not the GlobalKey value itself.

func (*BuildOwner) ScheduleBuild

func (b *BuildOwner) ScheduleBuild(element Element)

ScheduleBuild marks an element as needing rebuild.

func (*BuildOwner) UnregisterGlobalKey

func (b *BuildOwner) UnregisterGlobalKey(key any, element Element)

UnregisterGlobalKey removes a global key registration, but only if the currently registered element matches the provided element. This guard prevents a stale unmount from removing a registration that has already been claimed by a newly mounted element with the same key.

type Derived

Derived is a read-only reactive value that recomputes from source signals automatically. When any dependency fires its Listenable callback, the compute function is re-evaluated. If the new value differs from the previous one (checked via equality), all listeners are notified.

Derived is safe for concurrent use. Reads use a shared lock and writes (recomputation) use an exclusive lock, following the same RWMutex pattern as Signal.

Derived satisfies Listenable via Derived.AddListener. Derived values can be chained: one Derived can serve as a dependency of another.

Equality

By default, the old and new values are compared with interface comparison (any(old) != any(new)). This works for all comparable types (int, string, structs with comparable fields, etc.). For non-comparable types such as slices or maps, provide a custom equality function via NewDerivedWithEquality to avoid a runtime panic.

Lifecycle

A Derived subscribes to its dependencies on creation. Call Derived.Dispose to unsubscribe from all dependencies and release resources. Inside a stateful widget, use UseDerived to create, subscribe, and auto-dispose a Derived in one step.

Example

firstName := core.NewSignal("John")
lastName := core.NewSignal("Doe")
fullName := core.NewDerived(func() string {
return firstName.Value() + " " + lastName.Value()
}, firstName, lastName)

fmt.Println(fullName.Value()) // "John Doe"

lastName.Set("Smith")
fmt.Println(fullName.Value()) // "John Smith"
type Derived[T any] struct {
// contains filtered or unexported fields
}

func NewDerived

func NewDerived[T comparable](compute func() T, deps ...Listenable) *Derived[T]

NewDerived creates a Derived that recomputes when any dep changes. The compute function is called immediately to establish the initial value, then called again each time a dependency notifies.

Values are compared with == to skip redundant notifications. For non-comparable types (slices, maps), use NewDerivedWithEquality instead; the comparable constraint here ensures a compile-time error rather than a runtime panic.

func NewDerivedWithEquality

func NewDerivedWithEquality[T any](compute func() T, equal func(a, b T) bool, deps ...Listenable) *Derived[T]

NewDerivedWithEquality creates a Derived with a custom equality function for comparing old and new computed values. If equal is nil, the default interface comparison is used.

Use this when T is non-comparable (slices, maps) or when you need semantic equality that differs from Go's == operator:

items := core.NewDerivedWithEquality(
func() []string { return buildList(source.Value()) },
slices.Equal,
source,
)

func UseDerived

func UseDerived[T comparable](s stateBase, compute func() T, deps ...Listenable) *Derived[T]

UseDerived creates a Derived, subscribes to it for rebuilds, and auto-disposes it when the state is disposed. This is a convenience that combines NewDerived + UseListenable + OnDispose(d.Dispose) in one call.

Call this once in InitState, not in Build. Like UseListenable, the listener calls StateBase.SetState, so dependencies must only be mutated on the UI thread (use drift.Dispatch from goroutines).

Example:

type myState struct {
core.StateBase
firstName *core.Signal[string]
lastName *core.Signal[string]
fullName *core.Derived[string]
}

func (s *myState) InitState() {
s.firstName = core.NewSignal("John")
s.lastName = core.NewSignal("Doe")
s.fullName = core.UseDerived(s, func() string {
return s.firstName.Value() + " " + s.lastName.Value()
}, s.firstName, s.lastName)
}

func (s *myState) Build(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: s.fullName.Value()}
}

func UseDerivedWithEquality

func UseDerivedWithEquality[T any](s stateBase, compute func() T, equal func(a, b T) bool, deps ...Listenable) *Derived[T]

UseDerivedWithEquality is like UseDerived but accepts a custom equality function for comparing computed values. Use this when the derived type is non-comparable (slices, maps):

func (s *myState) InitState() {
s.tags = core.UseDerivedWithEquality(s, func() []string {
return buildTagList(s.source.Value())
}, slices.Equal, s.source)
}

func (*Derived[T]) AddListener

func (d *Derived[T]) AddListener(fn func()) func()

AddListener adds a callback that fires when the derived value changes. Use Derived.Value to read the new value inside the callback. This method satisfies the Listenable interface, enabling chained derivations where one Derived is a dependency of another. Returns an unsubscribe function.

func (*Derived[T]) Dispose

func (d *Derived[T]) Dispose()

Dispose unsubscribes from all dependencies and clears listeners. After disposal, the Derived will no longer recompute and any outstanding listener references become no-ops. Dispose is safe to call multiple times.

func (*Derived[T]) ListenerCount

func (d *Derived[T]) ListenerCount() int

ListenerCount returns the number of registered listeners.

func (*Derived[T]) Value

func (d *Derived[T]) Value() T

Value returns the current derived value. Safe for concurrent use.

type Disposable

Disposable is an interface for types that need cleanup.

type Disposable interface {
Dispose()
}

type Element

Element is the instantiation of a Widget at a specific location in the tree.

While widgets are immutable configuration objects, elements are the mutable counterparts that manage the widget's lifecycle in the tree. Each widget creates an element when mounted, and the element persists across widget rebuilds as long as the widget type and key match.

Elements form a tree parallel to the widget tree. When a parent widget rebuilds, the framework compares new widgets against existing elements to determine whether to update, replace, or remove elements.

Most developers don't interact with elements directly - the framework manages them automatically. Custom elements are only needed for advanced use cases like building new layout primitives.

type Element interface {
// Widget returns the current widget configuration for this element.
Widget() Widget
// Mount attaches this element to the tree under the given parent.
Mount(parent Element, slot any)
// Update reconfigures this element with a new widget of the same type.
Update(newWidget Widget)
// Unmount removes this element from the tree permanently.
Unmount()
// MarkNeedsBuild schedules this element for rebuild in the next frame.
MarkNeedsBuild()
// RebuildIfNeeded performs the rebuild if this element is marked dirty.
RebuildIfNeeded()
// VisitChildren calls the visitor for each child element.
VisitChildren(visitor func(Element) bool)
// Depth returns this element's depth in the tree (root is 0).
Depth() int
// Slot returns the slot identifier for this element in its parent.
Slot() any
// UpdateSlot changes the slot identifier for this element.
UpdateSlot(newSlot any)
}

func MountRoot

func MountRoot(root Widget, owner *BuildOwner) Element

MountRoot inflates and mounts the root widget with the provided build owner.

type ErrorBoundaryCapture

ErrorBoundaryCapture is implemented by error boundary elements to capture errors from descendant widgets.

type ErrorBoundaryCapture interface {
// CaptureError captures an error from a descendant widget.
// Returns true if the error was captured and handled.
CaptureError(err *errors.BoundaryError) bool
}

type ErrorWidgetBuilder

ErrorWidgetBuilder creates a fallback widget when a widget build fails. The builder receives the boundary error and should return a widget to display in place of the failed widget.

type ErrorWidgetBuilder func(err *errors.BoundaryError) Widget

func GetErrorWidgetBuilder

func GetErrorWidgetBuilder() ErrorWidgetBuilder

GetErrorWidgetBuilder returns the current error widget builder.

type GlobalKey

GlobalKey provides cross-tree access to an element's State and BuildContext. The type parameter S is the concrete State type returned by GlobalKey.CurrentState.

A GlobalKey is a value type that wraps a pointer; identity comes from the pointer, so two GlobalKey values wrapping the same pointer compare as equal via reflect.DeepEqual (the mechanism used by the framework's widget reconciliation). Two calls to NewGlobalKey always produce distinct keys.

When a widget returns a GlobalKey from its Key() method, the framework registers the element in the BuildOwner's global key registry on mount and removes it on unmount. This enables looking up the element, its state, or its context from anywhere in the application, regardless of tree position.

GlobalKey works with all element types (stateless, stateful, render object, inherited). However, GlobalKey.CurrentState only returns a non-zero value for stateful elements whose State satisfies the type parameter S.

Thread Safety

GlobalKey's accessor methods (CurrentState, CurrentElement, CurrentContext) read from fields that are written during mount/unmount on the UI thread. Call these methods from the UI thread only.

Example

var formKey = core.NewGlobalKey[*formState]()

type formWidget struct {
core.StatefulBase
}

func (w formWidget) Key() any { return formKey }
func (formWidget) CreateState() core.State { return &formState{} }

type formState struct {
core.StateBase
// ...
}

func (s *formState) Validate() bool { /* ... */ return true }

// From a sibling or parent widget, without passing references:
if state := formKey.CurrentState(); state != nil {
state.Validate()
}
type GlobalKey[S State] struct {
// contains filtered or unexported fields
}

func NewGlobalKey

func NewGlobalKey[S State]() GlobalKey[S]

NewGlobalKey creates a new GlobalKey with a unique identity. Each call returns a key that is distinct from every other key, so widget reconciliation will never match two widgets with different GlobalKey instances.

Store the key in a package-level variable or a long-lived struct field so that the same key is used across rebuilds:

var myKey = core.NewGlobalKey[*myState]()

func (GlobalKey[S]) CurrentContext

func (k GlobalKey[S]) CurrentContext() BuildContext

CurrentContext returns the BuildContext for this key's element, or nil. The returned context is valid only while the element is mounted.

func (GlobalKey[S]) CurrentElement

func (k GlobalKey[S]) CurrentElement() Element

CurrentElement returns the Element currently mounted with this key, or nil.

func (GlobalKey[S]) CurrentState

func (k GlobalKey[S]) CurrentState() S

CurrentState returns the State associated with this key's element, or the zero value of S if no element is currently mounted with this key. For non-stateful elements (stateless, render object, inherited) this always returns the zero value.

type IndexedSlot

IndexedSlot represents a child's position in a multi-child parent.

type IndexedSlot struct {
Index int
PreviousSibling Element
}

type InheritedBase

InheritedBase provides default CreateElement and Key implementations for inherited widgets. Embed it in your widget struct along with a Child field and implement [InheritedWidget.ShouldRebuildDependents] and [InheritedWidget.ChildWidget]:

type UserScope struct {
core.InheritedBase
User *User
Child core.Widget
}

func (u UserScope) ChildWidget() core.Widget { return u.Child }

func (u UserScope) ShouldRebuildDependents(old core.InheritedWidget) bool {
return u.User != old.(UserScope).User
}
type InheritedBase struct{}

func (InheritedBase) CreateElement

func (InheritedBase) CreateElement() Element

CreateElement returns a new InheritedElement.

func (InheritedBase) Key

func (InheritedBase) Key() any

Key returns nil (no key).

type InheritedElement

InheritedElement is the element that hosts an InheritedWidget and manages the dependency tracking for descendant widgets.

When a descendant calls [BuildContext.DependOnInherited], it registers as a dependent of this element. When the InheritedWidget is updated and [InheritedWidget.ShouldRebuildDependents] returns true, all registered dependents are notified and scheduled for rebuild.

Aspect\-Based Tracking

InheritedElement supports granular dependency tracking via aspects. When a dependent registers with a specific aspect (non-nil), it's stored in that dependent's aspect set. On update, if the widget implements AspectAwareInheritedWidget, ShouldRebuildDependent is called for each dependent to determine if it should rebuild based on its registered aspects.

Note: Aspect sets only grow during an element's lifetime. If a widget stops depending on an aspect across rebuilds, the old aspect remains registered. This may cause extra rebuilds but is safe (over-notification, not under).

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

func NewInheritedElement

func NewInheritedElement() *InheritedElement

NewInheritedElement creates an InheritedElement. The widget and build owner are set later by the framework during inflation.

func (*InheritedElement) AddDependent

func (e *InheritedElement) AddDependent(dependent Element, aspect any)

AddDependent registers an element as depending on this inherited widget. If aspect is non-nil, it's added to the dependent's aspect set for granular tracking. If aspect is nil, a sentinel is added indicating the widget depends on all changes.

Note: Aspect sets only grow during an element's lifetime. If a widget changes which aspects it depends on across rebuilds, old aspects remain registered. This may cause extra rebuilds but is safe (over-notification, not under-notification).

func (*InheritedElement) Mount

func (e *InheritedElement) Mount(parent Element, slot any)

func (*InheritedElement) MountWithSelf

func (e *InheritedElement) MountWithSelf(parent Element, slot any, self Element)

MountWithSelf allows a wrapper element to specify itself as the parent for children.

func (*InheritedElement) RebuildIfNeeded

func (e *InheritedElement) RebuildIfNeeded()

func (*InheritedElement) RebuildIfNeededWithSelf

func (e *InheritedElement) RebuildIfNeededWithSelf(self Element)

RebuildIfNeededWithSelf allows a wrapper element to specify itself as the parent.

func (*InheritedElement) RemoveDependent

func (e *InheritedElement) RemoveDependent(dependent Element)

RemoveDependent unregisters an element as depending on this inherited widget.

func (*InheritedElement) RenderObject

func (e *InheritedElement) RenderObject() layout.RenderObject

RenderObject returns the render object from the child element.

func (*InheritedElement) Unmount

func (e *InheritedElement) Unmount()

func (*InheritedElement) Update

func (e *InheritedElement) Update(newWidget Widget)

func (*InheritedElement) VisitChildren

func (e *InheritedElement) VisitChildren(visitor func(Element) bool)

type InheritedProvider

InheritedProvider is a generic inherited widget that eliminates boilerplate for simple data-down-the-tree patterns.

Example usage:

// Provide a value
core.InheritedProvider[*User]{
Value: currentUser,
Child: MainContent{},
}

// Consume from anywhere in the subtree
if user, ok := core.Provide[*User](ctx); ok {
// use user
}

For custom comparison logic, set ShouldRebuild:

core.InheritedProvider[*User]{
Value: currentUser,
Child: MainContent{},
ShouldRebuild: func(old, new *User) bool {
return old.ID != new.ID // Only rebuild on ID change
},
}

Note: The default comparison uses == which panics for non-comparable types (slices, maps, funcs). For these types, you must provide a ShouldRebuild function.

For more advanced use cases like aspect-based tracking, implement a custom InheritedWidget instead.

type InheritedProvider[T any] struct {
// Value is the data to provide to descendants.
Value T

// Child is the child widget tree.
Child Widget

// WidgetKey is an optional key for widget identity.
WidgetKey any

// ShouldRebuild is an optional function to customize when dependents rebuild.
// If nil, defaults to value inequality (any(old) != any(new)).
// Required for non-comparable types (slices, maps, funcs) to avoid panics.
ShouldRebuild func(old, new T) bool
}

func (InheritedProvider[T]) ChildWidget

func (p InheritedProvider[T]) ChildWidget() Widget

ChildWidget implements InheritedWidget.

func (InheritedProvider[T]) CreateElement

func (p InheritedProvider[T]) CreateElement() Element

CreateElement implements Widget.

func (InheritedProvider[T]) Key

func (p InheritedProvider[T]) Key() any

Key implements Widget.

func (InheritedProvider[T]) ShouldRebuildDependents

func (p InheritedProvider[T]) ShouldRebuildDependents(oldWidget InheritedWidget) bool

ShouldRebuildDependents implements InheritedWidget. Returns true if dependents should rebuild when this widget is updated.

type InheritedWidget

InheritedWidget provides data to descendant widgets without explicit parameter passing through the widget tree.

InheritedWidget is the foundation for dependency injection in the widget tree. Descendant widgets can access the inherited data via [BuildContext.DependOnInherited], and they automatically rebuild when the inherited data changes.

When to Use

Use InheritedWidget when data needs to be accessed by many widgets at different levels of the tree. Common examples include:

  • Theme data (colors, typography)
  • Localization strings
  • User authentication state
  • Application configuration

Simple Usage with InheritedProvider

For simple cases, use the generic InheritedProvider which eliminates boilerplate:

// Provide data
core.InheritedProvider[*UserState]{
Value: userState,
Child: MyApp{},
}

// Access data (in a descendant's Build method)
if user, ok := core.Provide[*UserState](ctx); ok {
// use user
}

Custom Implementation

Embed InheritedBase and implement only ShouldRebuildDependents:

type UserScope struct {
core.InheritedBase
User *User
}

func (u UserScope) ShouldRebuildDependents(old core.InheritedWidget) bool {
return u.User != old.(UserScope).User
}

When [ShouldRebuildDependents] returns true, all dependents are notified. For fine-grained per-dependent filtering based on registered aspects, implement the optional AspectAwareInheritedWidget interface.

type InheritedWidget interface {
Widget
// ChildWidget returns the child widget tree.
ChildWidget() Widget
// ShouldRebuildDependents returns true if dependents should rebuild when this
// widget is updated. When true, all dependents are notified unless the widget
// also implements [AspectAwareInheritedWidget] for per-dependent filtering.
ShouldRebuildDependents(oldWidget InheritedWidget) bool
}

type LayoutBuilderElement

LayoutBuilderElement hosts a LayoutBuilderWidget, deferring child building to the layout phase so the builder function receives actual constraints.

This element uses a dual-trigger invalidation model:

  • Layout-phase trigger: when the parent's constraints change, the render object calls layoutCallback during PerformLayout and the element re-invokes the builder with the new constraints.
  • Build-phase trigger: when an inherited dependency changes or the widget is updated, LayoutBuilderElement.RebuildIfNeeded translates the dirty flag into childDirty and calls MarkNeedsLayout on the render object, which schedules a new layout pass that re-invokes the builder.

LayoutBuilderElement implements [renderObjectHost], allowing descendant RenderObjectElements to attach their render objects through it.

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

func NewLayoutBuilderElement

func NewLayoutBuilderElement(widget LayoutBuilderWidget, owner *BuildOwner) *LayoutBuilderElement

NewLayoutBuilderElement creates a LayoutBuilderElement for the given widget. The owner may be nil during widget testing; it is set by the framework when the element is mounted into a live tree.

func (*LayoutBuilderElement) Mount

func (e *LayoutBuilderElement) Mount(parent Element, slot any)

Mount creates the render object, registers the layout callback on it, attaches to the render tree, and marks the child as needing its first build. Child building is deferred until the first layout pass.

func (*LayoutBuilderElement) RebuildIfNeeded

func (e *LayoutBuilderElement) RebuildIfNeeded()

RebuildIfNeeded handles build-phase invalidation (e.g. inherited dependency changes). Child building is still deferred to layout, but we must translate the dirty flag into childDirty + MarkNeedsLayout so the layout callback re-invokes the builder.

func (*LayoutBuilderElement) RenderObject

func (e *LayoutBuilderElement) RenderObject() layout.RenderObject

RenderObject returns the render object owned by this element.

func (*LayoutBuilderElement) Unmount

func (e *LayoutBuilderElement) Unmount()

Unmount recursively unmounts the child element and detaches the render object from the render tree.

func (*LayoutBuilderElement) Update

func (e *LayoutBuilderElement) Update(newWidget Widget)

Update replaces the widget, marks the child dirty, and triggers a relayout so the layout callback re-invokes the builder with the new widget's function.

func (*LayoutBuilderElement) UpdateSlot

func (e *LayoutBuilderElement) UpdateSlot(newSlot any)

UpdateSlot updates the slot and notifies the render parent of the move.

func (*LayoutBuilderElement) VisitChildren

func (e *LayoutBuilderElement) VisitChildren(visitor func(Element) bool)

VisitChildren calls the visitor with the single child element, if present.

type LayoutBuilderWidget

LayoutBuilderWidget is implemented by widgets that defer child building to the layout phase. Unlike standard widgets whose Build runs before layout, a LayoutBuilderWidget provides a builder function that is invoked during the render object's PerformLayout, once the parent's constraints are known.

The LayoutBuilder method returns the builder callback. The element stores this callback and passes it to the render object, which invokes it with the resolved constraints during layout.

type LayoutBuilderWidget interface {
RenderObjectWidget
LayoutBuilder() func(ctx BuildContext, constraints layout.Constraints) Widget
}

type Listenable

Listenable is an interface for types that support untyped change notification. Signal, Derived, and Notifier all satisfy this interface.

Listenable is the dependency type accepted by NewDerived and UseDerived. AddListener should return an unsubscribe function.

type Listenable interface {
AddListener(listener func()) func()
}

type ListenableBuilder

ListenableBuilder is a convenience StatefulWidget that rebuilds whenever a Listenable notifies. It collapses the common "subscribe + SetState" pattern into a single struct literal:

core.ListenableBuilder{
Listenable: counter,
Builder: func(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: fmt.Sprint(counter.Value())}
},
}

For multiple listenables, merge them with NewDerived or use a full StatefulWidget.

type ListenableBuilder struct {
StatefulBase
Listenable Listenable
Builder func(ctx BuildContext) Widget
}

func (ListenableBuilder) CreateState

func (ListenableBuilder) CreateState() State

type Notifier

Notifier provides listener management and disposal for custom state holders. Embed this struct to get reactive notification for free.

Example:

type AuthNotifier struct {
core.Notifier
isLoggedIn bool
}

func (n *AuthNotifier) SetLoggedIn(v bool) {
n.isLoggedIn = v
n.Notify()
}
type Notifier struct {
// contains filtered or unexported fields
}

Example:

This example shows how to create a custom notifier.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/core"
)

func main() {
notifier := &core.Notifier{}
unsub := notifier.AddListener(func() {
fmt.Println("Notifier triggered")
})
notifier.Notify()
unsub()
notifier.Dispose()

}

Output

Notifier triggered

func (*Notifier) AddListener

func (c *Notifier) AddListener(fn func()) func()

AddListener adds a callback that fires when Notify() is called. Returns an unsubscribe function.

func (*Notifier) Dispose

func (c *Notifier) Dispose()

Dispose clears all listeners and marks the notifier as disposed. Override this method if you need custom cleanup, but always call c.Notifier.Dispose() in your override.

func (*Notifier) IsDisposed

func (c *Notifier) IsDisposed() bool

IsDisposed returns true if this notifier has been disposed.

func (*Notifier) ListenerCount

func (c *Notifier) ListenerCount() int

ListenerCount returns the number of registered listeners.

func (*Notifier) Notify

func (c *Notifier) Notify()

Notify calls all registered listeners. Safe to call after disposal (becomes a no-op).

type RenderObjectBase

RenderObjectBase provides default CreateElement and Key implementations for render object widgets. Embed it in your widget struct to satisfy the Widget interface without boilerplate:

type MyWidget struct {
core.RenderObjectBase
Child core.Widget
}

func (w MyWidget) ChildWidget() core.Widget { return w.Child }

func (w MyWidget) CreateRenderObject(ctx core.BuildContext) layout.RenderObject { ... }

func (w MyWidget) UpdateRenderObject(ctx core.BuildContext, ro layout.RenderObject) { ... }
type RenderObjectBase struct{}

func (RenderObjectBase) CreateElement

func (RenderObjectBase) CreateElement() Element

CreateElement returns a new RenderObjectElement.

func (RenderObjectBase) Key

func (RenderObjectBase) Key() any

Key returns nil (no key).

type RenderObjectElement

RenderObjectElement hosts a RenderObject and optional children.

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

func NewRenderObjectElement

func NewRenderObjectElement() *RenderObjectElement

func (*RenderObjectElement) Mount

func (e *RenderObjectElement) Mount(parent Element, slot any)

func (*RenderObjectElement) RebuildIfNeeded

func (e *RenderObjectElement) RebuildIfNeeded()

func (*RenderObjectElement) RenderObject

func (e *RenderObjectElement) RenderObject() layout.RenderObject

RenderObject exposes the backing render object for the element.

func (*RenderObjectElement) Unmount

func (e *RenderObjectElement) Unmount()

func (*RenderObjectElement) Update

func (e *RenderObjectElement) Update(newWidget Widget)

func (*RenderObjectElement) UpdateSlot

func (e *RenderObjectElement) UpdateSlot(newSlot any)

UpdateSlot updates the slot and notifies the render parent of the move.

func (*RenderObjectElement) VisitChildren

func (e *RenderObjectElement) VisitChildren(visitor func(Element) bool)

type RenderObjectWidget

RenderObjectWidget creates a render object directly.

type RenderObjectWidget interface {
Widget
CreateRenderObject(ctx BuildContext) layout.RenderObject
UpdateRenderObject(ctx BuildContext, renderObject layout.RenderObject)
}

type ScrollOffsetProvider

ScrollOffsetProvider reports a paint-time scroll offset for descendants.

type ScrollOffsetProvider interface {
ScrollOffset() graphics.Offset
}

type Signal

Signal holds a value and notifies listeners when it changes. It is safe for concurrent use.

Signal satisfies Listenable via Signal.AddListener.

Example:

count := core.NewSignal(0)
unsub := count.AddListener(func() {
fmt.Println("Count changed to:", count.Value())
})
count.Set(5) // prints: Count changed to: 5
unsub() // stop listening
type Signal[T any] struct {
// contains filtered or unexported fields
}

Example:

This example shows how to create a Signal for reactive state. Signal is thread-safe and can be shared across goroutines.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/core"
)

func main() {
// Create a signal with an initial value
counter := core.NewSignal(0)

// Add a listener that fires when the value changes
unsub := counter.AddListener(func() {
fmt.Printf("Counter changed to: %d\n", counter.Value())
})

// Update the value - this triggers all listeners
counter.Set(5)

// Read the current value
current := counter.Value()
fmt.Printf("Current value: %d\n", current)

// Clean up when done
unsub()

}

Output

Counter changed to: 5
Current value: 5

func NewSignal

func NewSignal[T comparable](initial T) *Signal[T]

NewSignal creates a new signal with the given initial value. Signal.Set skips notification when the new value equals the old via ==. For non-comparable types (slices, maps), use NewSignalWithEquality instead; the comparable constraint here ensures a compile-time error rather than a runtime panic.

func NewSignalWithEquality

func NewSignalWithEquality[T any](initial T, equalityFunc func(a, b T) bool) *Signal[T]

NewSignalWithEquality creates a new signal with a custom equality function. Use this for non-comparable types (slices, maps) or when the default interface comparison is not appropriate (e.g. comparing only specific fields).

Example:

This example shows how to use Signal with a custom equality function. This is useful when you want to avoid unnecessary updates.

package main

import (
"fmt"

"github.com/go-drift/drift/pkg/core"
)

func main() {
type User struct {
ID int
Name string
}

// Only notify listeners when the user ID changes
user := core.NewSignalWithEquality(User{ID: 1, Name: "Alice"}, func(a, b User) bool {
return a.ID == b.ID
})

user.AddListener(func() {
fmt.Printf("User changed: %s\n", user.Value().Name)
})

// This won't trigger listeners because ID is the same
user.Set(User{ID: 1, Name: "Alice Updated"})

// This will trigger listeners because ID changed
user.Set(User{ID: 2, Name: "Bob"})

}

Output

User changed: Bob

func (*Signal[T]) AddListener

func (s *Signal[T]) AddListener(fn func()) func()

AddListener adds a callback that fires when the value changes. Use Signal.Value to read the new value inside the callback. This method satisfies the Listenable interface. Returns an unsubscribe function.

func (*Signal[T]) Dispose

func (s *Signal[T]) Dispose()

Dispose clears all listeners and marks the signal as disposed. After disposal, Signal.Set, Signal.Update, and Signal.AddListener become no-ops. Signal.Value continues to return the last set value. Dispose is safe to call multiple times.

func (*Signal[T]) IsDisposed

func (s *Signal[T]) IsDisposed() bool

IsDisposed returns true if this signal has been disposed.

func (*Signal[T]) ListenerCount

func (s *Signal[T]) ListenerCount() int

ListenerCount returns the number of registered listeners.

func (*Signal[T]) Set

func (s *Signal[T]) Set(value T)

Set updates the value and notifies all listeners. Notification is skipped when the old and new values are equal. By default, values are compared with interface comparison (any(old) == any(new)), which works for all comparable types. For non-comparable types (slices, maps), provide a custom equality function via NewSignalWithEquality to avoid a runtime panic.

func (*Signal[T]) Update

func (s *Signal[T]) Update(transform func(T) T)

Update applies a transformation to the current value. This is useful for complex updates that depend on the current value. Like Signal.Set, notification is skipped when the value does not change.

func (*Signal[T]) Value

func (s *Signal[T]) Value() T

Value returns the current value.

type State

State holds mutable state for a StatefulWidget.

State objects are long-lived - they persist across widget rebuilds as long as the widget remains in the tree at the same location. This allows state like user input, scroll position, or animation values to be preserved.

Lifecycle

State lifecycle methods are called in this order:

  1. SetElement - Framework sets the element reference
  2. InitState - Called once when state is first created
  3. DidChangeDependencies - Called after InitState and whenever inherited dependencies change
  4. Build - Called to create the widget tree (may be called many times)
  5. DidUpdateWidget - Called when parent rebuilds with new widget configuration
  6. Dispose - Called when the widget is permanently removed from the tree

Triggering Rebuilds

Call SetState with a function that modifies state to schedule a rebuild:

s.SetState(func() {
s.count++
})
type State interface {
// InitState is called once when the state is first created.
// Use this to initialize state that depends on the widget configuration.
InitState()
// Build creates the widget tree. Called whenever the state changes or
// the parent rebuilds with new widget configuration.
Build(ctx BuildContext) Widget
// SetState schedules a rebuild after executing the provided function.
// The function should modify state variables that affect the Build output.
SetState(fn func())
// Dispose is called when the widget is permanently removed from the tree.
// Use this to release resources like stream subscriptions or controllers.
Dispose()
// DidChangeDependencies is called after InitState on initial mount,
// and again whenever an InheritedWidget ancestor notifies of changes.
DidChangeDependencies()
// DidUpdateWidget is called when the parent rebuilds with a new widget
// configuration. The old widget is passed for comparison.
DidUpdateWidget(oldWidget StatefulWidget)
}

type StateBase

StateBase provides common functionality for stateful widget states. Embed this struct in your state to eliminate boilerplate.

Example:

type myState struct {
core.StateBase
count int
}

func (s *myState) InitState() {
// No need to implement SetElement, SetState, Dispose, etc.
}
type StateBase struct {
// contains filtered or unexported fields
}

func (*StateBase) Build

func (s *StateBase) Build(ctx BuildContext) Widget

Build is a no-op default implementation that returns nil. Override this method to build your widget tree.

func (*StateBase) DidChangeDependencies

func (s *StateBase) DidChangeDependencies()

DidChangeDependencies is a no-op default implementation. Override this method to respond to inherited widget changes.

func (*StateBase) DidUpdateWidget

func (s *StateBase) DidUpdateWidget(oldWidget StatefulWidget)

DidUpdateWidget is a no-op default implementation. Override this method to respond to widget configuration changes.

func (*StateBase) Dispose

func (s *StateBase) Dispose()

Dispose cleans up resources. Override this method if you need custom cleanup, but always call s.RunDisposers() or s.StateBase.Dispose() in your override.

func (*StateBase) Element

func (s *StateBase) Element() *StatefulElement

Element returns the element associated with this state. Returns nil if the state has been disposed or not yet mounted.

func (*StateBase) InitState

func (s *StateBase) InitState()

InitState is a no-op default implementation. Override this method to initialize your state.

func (*StateBase) IsDisposed

func (s *StateBase) IsDisposed() bool

IsDisposed returns true if this state has been disposed.

func (*StateBase) OnDispose

func (s *StateBase) OnDispose(cleanup func()) func()

OnDispose registers a cleanup function to be called when the state is disposed. Returns an unregister function that can be called to remove the disposer. The cleanup function will only be called once.

func (*StateBase) RunDisposers

func (s *StateBase) RunDisposers()

RunDisposers executes all registered disposers in reverse order. This is called automatically by Dispose().

func (*StateBase) SetElement

func (s *StateBase) SetElement(element *StatefulElement)

SetElement stores the element reference for triggering rebuilds. This method is called automatically by the framework.

func (*StateBase) SetState

func (s *StateBase) SetState(fn func())

SetState executes the given function and schedules a rebuild. Safe to call even after disposal (becomes a no-op).

SetState is NOT thread-safe. It must only be called from the UI thread. To update state from a background goroutine, use drift.Dispatch.

type StatefulBase

StatefulBase provides default CreateElement and Key implementations for stateful widgets. Embed it in your widget struct to satisfy the Widget interface without boilerplate:

type Counter struct {
core.StatefulBase
}

func (Counter) CreateState() core.State { return &counterState{} }
type StatefulBase struct{}

func (StatefulBase) CreateElement

func (StatefulBase) CreateElement() Element

CreateElement returns a new StatefulElement.

func (StatefulBase) Key

func (StatefulBase) Key() any

Key returns nil (no key).

type StatefulElement

StatefulElement hosts a StatefulWidget and its State.

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

func NewStatefulElement

func NewStatefulElement() *StatefulElement

func (*StatefulElement) Mount

func (e *StatefulElement) Mount(parent Element, slot any)

func (*StatefulElement) RebuildIfNeeded

func (e *StatefulElement) RebuildIfNeeded()

func (*StatefulElement) RenderObject

func (e *StatefulElement) RenderObject() layout.RenderObject

RenderObject returns the render object from the first render-object child.

func (*StatefulElement) Unmount

func (e *StatefulElement) Unmount()

func (*StatefulElement) Update

func (e *StatefulElement) Update(newWidget Widget)

func (*StatefulElement) VisitChildren

func (e *StatefulElement) VisitChildren(visitor func(Element) bool)

type StatefulWidget

StatefulWidget has mutable state that persists across rebuilds.

Use StatefulWidget when the widget needs to maintain state that can change during its lifetime, such as user input, animation values, or data fetched asynchronously.

StatefulWidget creates a State object that holds the mutable data. The State persists even when the widget is rebuilt with new configuration, allowing it to maintain continuity.

Embed StatefulBase to satisfy the Widget interface automatically:

type Counter struct {
core.StatefulBase
}

func (Counter) CreateState() core.State { return &counterState{} }

type counterState struct {
core.StateBase
count int
}

func (s *counterState) Build(ctx core.BuildContext) core.Widget {
return widgets.Button{
Label: fmt.Sprintf("Count: %d", s.count),
OnTap: func() {
s.SetState(func() { s.count++ })
},
}
}
type StatefulWidget interface {
Widget
// CreateState creates the mutable state for this widget.
CreateState() State
}

type StatelessBase

StatelessBase provides default CreateElement and Key implementations for stateless widgets. Embed it in your widget struct to satisfy the Widget interface without boilerplate:

type Greeting struct {
core.StatelessBase
Name string
}

func (g Greeting) Build(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: "Hello, " + g.Name}
}
type StatelessBase struct{}

func (StatelessBase) CreateElement

func (StatelessBase) CreateElement() Element

CreateElement returns a new StatelessElement.

func (StatelessBase) Key

func (StatelessBase) Key() any

Key returns nil (no key).

type StatelessElement

StatelessElement hosts a StatelessWidget.

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

func NewStatelessElement

func NewStatelessElement() *StatelessElement

func (*StatelessElement) Mount

func (e *StatelessElement) Mount(parent Element, slot any)

func (*StatelessElement) RebuildIfNeeded

func (e *StatelessElement) RebuildIfNeeded()

func (*StatelessElement) RenderObject

func (e *StatelessElement) RenderObject() layout.RenderObject

RenderObject returns the render object from the first render-object child.

func (*StatelessElement) Unmount

func (e *StatelessElement) Unmount()

func (*StatelessElement) Update

func (e *StatelessElement) Update(newWidget Widget)

func (*StatelessElement) VisitChildren

func (e *StatelessElement) VisitChildren(visitor func(Element) bool)

type StatelessWidget

StatelessWidget builds UI without mutable state.

Use StatelessWidget when the widget's appearance depends only on its configuration (the struct fields) and inherited data from ancestors. The widget rebuilds when its parent rebuilds with different configuration or when an inherited dependency changes.

Embed StatelessBase to satisfy the Widget interface automatically:

type Greeting struct {
core.StatelessBase
Name string
}

func (g Greeting) Build(ctx core.BuildContext) core.Widget {
return widgets.Text{Content: "Hello, " + g.Name}
}
type StatelessWidget interface {
Widget
// Build creates the widget tree for this widget's portion of the UI.
Build(ctx BuildContext) Widget
}

type Widget

Widget is an immutable description of part of the user interface.

Widgets are the building blocks of the UI. They describe what the interface should look like given the current configuration and state. Widgets themselves are immutable - when the UI needs to change, new widget instances are created.

There are three main types of widgets:

Widgets create Element instances that manage the actual UI lifecycle.

type Widget interface {
// CreateElement creates the element that will manage this widget's lifecycle.
CreateElement() Element
// Key returns an optional key for widget identity. When non-nil, the framework
// uses the key to match widgets across rebuilds, preserving state and avoiding
// unnecessary recreation. Use keys for widgets in lists or when widget identity
// matters across tree restructuring.
Key() any
}

func DefaultErrorWidgetBuilder

func DefaultErrorWidgetBuilder(err *errors.BoundaryError) Widget

DefaultErrorWidgetBuilder returns a placeholder widget when build fails. The actual error widget implementation is in pkg/widgets to avoid circular dependencies. This default returns nil, which signals the framework to use the widgets.ErrorWidget if available.

func Stateful

func Stateful[S any](init func() S, build func(state S, ctx BuildContext, setState func(func(S) S)) Widget) Widget

Stateful creates an inline stateful widget using closures. Use this for quick, self-contained UI fragments that don't need lifecycle hooks or StateBase features.

widget := core.Stateful(
func() int { return 0 },
func(count int, ctx core.BuildContext, setState func(func(int) int)) core.Widget {
return widgets.GestureDetector{
OnTap: func() {
setState(func(c int) int { return c + 1 })
},
Child: widgets.Text{Content: fmt.Sprintf("Count: %d", count)},
}
},
)

The generic parameter is the state type. setState takes a function that transforms the current state to a new state.

For complex widgets with many state fields, lifecycle methods, or UseDisposable, embed StatefulBase in a named struct instead.

Generated by gomarkdoc