state

package
v0.1.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package state provides reactive state management for the gogpu/ui widget tree.

It wraps github.com/coregx/signals with UI-specific helpers for binding reactive values to widget invalidation and for batching multiple state changes into a single render pass.

Core Concepts

A Signal holds a single value and notifies subscribers when that value changes. A Computed signal derives its value from other signals and recomputes lazily. An Effect runs a side-effect whenever its dependencies change. A Binding connects a signal to a widget so the widget is automatically invalidated (marked for re-render) when the signal changes. A Scheduler collects dirty widgets and flushes them in one batch.

Quick Start

// 1. Create a signal for some piece of UI state.
counter := state.NewSignal(0)

// 2. Derive a display string from it.
label := state.NewComputed(func() string {
    return fmt.Sprintf("Count: %d", counter.Get())
}, counter.AsReadonly())

// 3. Bind the signal to a widget so it re-renders on change.
binding := state.Bind(counter, ctx)
defer binding.Unbind()

// 4. Batch multiple updates into one render.
sched := state.NewScheduler(func(dirty []widget.Widget) {
    for _, w := range dirty {
        renderWidget(w)
    }
})
sched.Batch(func() {
    counter.Set(1)
    counter.Set(2)
    counter.Set(3)
})
sched.Flush() // processes all dirty widgets once

Thread Safety

All types in this package are safe for concurrent use. Scheduler and Binding protect their internal state with mutexes. The underlying signals library is also fully thread-safe.

Memory Safety

Every Binding must be cleaned up by calling Unbind when the widget is removed from the tree. Failing to do so leaks the subscription. Effects returned by NewEffect must be stopped via the returned EffectRef.Stop method.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Binding

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

Binding connects a reactive signal to a widget's invalidation lifecycle.

When the bound signal's value changes the widget's context is invalidated, which marks the widget for re-render. A Binding must be cleaned up via Binding.Unbind when the widget is removed from the tree; otherwise the subscription leaks.

Create a Binding with Bind.

func Bind

func Bind[T any](sig ReadonlySignal[T], ctx widget.Context) *Binding

Bind creates a Binding that invalidates ctx whenever sig changes.

The type parameter T must match the signal's value type. The binding subscribes to the signal using SubscribeForever; the caller must call Binding.Unbind to release the subscription.

Example:

counter := state.NewSignal(0)
binding := state.Bind(counter, ctx)
defer binding.Unbind()

counter.Set(1) // ctx.Invalidate() is called automatically

func BindToScheduler

func BindToScheduler[T any](sig ReadonlySignal[T], w widget.Widget, sched widget.SchedulerRef) *Binding

BindToScheduler creates a Binding that marks w as dirty in sched whenever sig changes.

Use this instead of Bind when you want fine-grained control over render batching. The scheduler collects dirty widgets and processes them in a single flush.

Example:

counter := state.NewSignal(0)
sched := state.NewScheduler(flushFn)
binding := state.BindToScheduler(counter, myWidget, sched)
defer binding.Unbind()

counter.Set(1) // sched.MarkDirty(myWidget) is called

func (*Binding) IsActive

func (b *Binding) IsActive() bool

IsActive reports whether the binding is still active (not yet unbound).

func (*Binding) Unbind

func (b *Binding) Unbind()

Unbind stops the binding so that future signal changes no longer invalidate the widget. Safe to call multiple times; subsequent calls are no-ops.

type EffectRef

type EffectRef = signals.EffectRef

EffectRef represents a running side effect that can be stopped.

Effects run immediately upon creation and re-run whenever any of their dependencies change. Call Stop to clean up the effect and unsubscribe from all dependencies.

func NewEffect

func NewEffect(fn func(), deps ...any) EffectRef

NewEffect creates a side effect that runs immediately and re-runs whenever any dependency changes.

Dependencies must be passed explicitly. The effect function should not return anything; for effects that need cleanup use NewEffectWithCleanup.

Example:

count := state.NewSignal(0)

eff := state.NewEffect(func() {
    fmt.Println("count is", count.Get())
}, count.AsReadonly())
defer eff.Stop()

count.Set(5) // prints "count is 5"

func NewEffectWithCleanup

func NewEffectWithCleanup(fn func() func(), deps ...any) EffectRef

NewEffectWithCleanup creates a side effect whose function returns a cleanup callback.

The cleanup callback is called:

  • Before the next execution of the effect
  • When Stop is called

This is useful for canceling timers, closing connections, or removing event listeners established by the effect.

Example:

interval := state.NewSignal(time.Second)

eff := state.NewEffectWithCleanup(func() func() {
    ticker := time.NewTicker(interval.Get())
    go func() {
        for range ticker.C {
            fmt.Println("tick")
        }
    }()
    return func() { ticker.Stop() }
}, interval.AsReadonly())
defer eff.Stop()

type EqualFunc

type EqualFunc[T any] = signals.EqualFunc[T]

EqualFunc is a custom equality function used to determine whether a signal's value has changed. When provided, Set only notifies subscribers if the new value is not equal to the old value according to this function.

type Options

type Options[T any] = signals.Options[T]

Options configures the behavior of a signal.

Equal — optional custom equality function; if nil every Set notifies. OnPanic — optional panic handler for subscriber callbacks.

type ReadonlySignal

type ReadonlySignal[T any] = signals.ReadonlySignal[T]

ReadonlySignal is a read-only view of a signal that supports Get and Subscribe but not Set or Update.

func NewComputed

func NewComputed[T any](compute func() T, deps ...any) ReadonlySignal[T]

NewComputed creates a read-only signal whose value is derived from the given computation function.

The compute function must be pure: it should read signals and return a result without side effects. Dependencies must be passed explicitly so that the computed signal is marked dirty when any dependency changes.

The computed value uses lazy evaluation and memoisation: it is only recomputed when accessed after a dependency change.

Example:

firstName := state.NewSignal("John")
lastName := state.NewSignal("Doe")

fullName := state.NewComputed(func() string {
    return firstName.Get() + " " + lastName.Get()
}, firstName.AsReadonly(), lastName.AsReadonly())

fmt.Println(fullName.Get()) // "John Doe"
firstName.Set("Jane")
fmt.Println(fullName.Get()) // "Jane Doe"

func NewComputedWithOptions

func NewComputedWithOptions[T any](compute func() T, opts Options[T], deps ...any) ReadonlySignal[T]

NewComputedWithOptions creates a computed signal with custom options.

Use this when you need custom panic handling for the compute function or its subscribers.

type Scheduler

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

Scheduler collects widgets that need re-rendering and flushes them in a single batch. This avoids redundant render passes when multiple signals change during the same event cycle.

Scheduler is instance-based (no global state) and safe for concurrent use.

Create a Scheduler with NewScheduler.

func NewScheduler

func NewScheduler(flushFn func([]widget.Widget)) *Scheduler

NewScheduler creates a Scheduler that calls flushFn with the deduplicated list of dirty widgets when Scheduler.Flush is invoked.

flushFn must not be nil. It receives a slice of unique widgets that were marked dirty since the last flush. The order of widgets in the slice is not guaranteed.

Example:

sched := state.NewScheduler(func(dirty []widget.Widget) {
    for _, w := range dirty {
        w.Layout(ctx, constraints)
        w.Draw(ctx, canvas)
    }
})

func (*Scheduler) Batch

func (s *Scheduler) Batch(fn func())

Batch groups multiple state changes so that no automatic flush happens until fn returns. After fn completes the pending widgets are NOT automatically flushed; call Scheduler.Flush explicitly when the render loop is ready.

Batch calls may be nested. The batching flag is reference-counted internally via a simple boolean; the outermost Batch call clears it.

Example:

sched.Batch(func() {
    counter.Set(1)
    name.Set("Alice")
    // Both changes enqueue dirty widgets, but nothing is flushed yet.
})
sched.Flush() // one flush for both changes

func (*Scheduler) Flush

func (s *Scheduler) Flush()

Flush processes all pending dirty widgets by calling the flush function provided to NewScheduler.

After the call the pending set is empty. If there are no pending widgets Flush is a no-op. Flush is safe to call from any goroutine.

Widgets added during the flush callback are not included in the current flush; they will be picked up by the next call to Flush.

func (*Scheduler) IsFlushing

func (s *Scheduler) IsFlushing() bool

IsFlushing reports whether the scheduler is currently executing its flush function. This is useful for re-entrancy guards.

func (*Scheduler) MarkDirty

func (s *Scheduler) MarkDirty(w widget.Widget)

MarkDirty queues a widget for re-render.

If the same widget is marked dirty multiple times before the next Scheduler.Flush it is only processed once (deduplication).

When the pending set transitions from empty to non-empty, the onDirty callback (if set via Scheduler.SetOnDirty) is invoked to wake the render loop.

If the scheduler is not currently inside a Scheduler.Batch call, MarkDirty is a lightweight enqueue operation. The actual flush happens when the render loop calls Flush.

func (*Scheduler) PendingCount

func (s *Scheduler) PendingCount() int

PendingCount returns the number of widgets currently awaiting flush.

This is primarily useful for testing and diagnostics.

func (*Scheduler) SetOnDirty

func (s *Scheduler) SetOnDirty(fn func())

SetOnDirty registers a callback that is invoked when the pending set transitions from empty to non-empty. This is typically used to wake the render loop (e.g., call RequestRedraw) so that a new frame is scheduled.

The callback is called outside the scheduler's lock and must be safe for concurrent use. Only one callback can be registered; subsequent calls replace the previous one. Pass nil to remove the callback.

type Signal

type Signal[T any] = signals.Signal[T]

Signal is a writable reactive value that notifies subscribers when changed.

It is a type alias for signals.Signal re-exported for convenience so that consumers of the state package do not need to import coregx/signals directly.

func NewSignal

func NewSignal[T any](initial T) Signal[T]

NewSignal creates a new writable signal with the given initial value.

The signal uses default behavior: no equality checks and default panic handling (log and continue).

Example:

count := state.NewSignal(0)
count.Set(5)
fmt.Println(count.Get()) // 5

func NewSignalWithOptions

func NewSignalWithOptions[T any](initial T, opts Options[T]) Signal[T]

NewSignalWithOptions creates a new writable signal with custom options.

Use this when you need custom equality checks or custom panic handling.

Example:

count := state.NewSignalWithOptions(0, state.Options[int]{
    Equal: func(a, b int) bool { return a == b },
})

type Unsubscribe

type Unsubscribe = signals.Unsubscribe

Unsubscribe is a function returned by Subscribe that removes the subscription. It must be called to prevent memory leaks.

func Subscribe

func Subscribe[T any](sig ReadonlySignal[T], ctx context.Context, fn func(T)) Unsubscribe

Subscribe registers a callback on a readable signal that is automatically canceled when the context is done. Returns an Unsubscribe function for manual cleanup.

This is a convenience wrapper so callers do not need to import coregx/signals.

func SubscribeForever

func SubscribeForever[T any](sig ReadonlySignal[T], fn func(T)) Unsubscribe

SubscribeForever registers a callback on a readable signal that is never automatically canceled. The caller must call the returned Unsubscribe to prevent memory leaks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL