Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions compose/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,26 @@ import (
type Composable = api.Composable
type Composer = api.Composer

// NewComposer creates a new Composer instance initialized with the given persistent state store.
// The returned Composer is the entry point for building and managing the composition tree.
//
// Parameters:
// - store: A PersistentState implementation that manages the state across recompositions.
//
// Returns:
// - A new Composer instance.
func NewComposer(store state.PersistentState) Composer {
return zipper.NewComposer(store)
}

// Sequence is a convenience function for combining multiple Composables into a single one.
// It delegates to the internal sequence implementation.
//
// Deprecated: Use c.Sequence(...) method on the Composer interface instead for better fluency.
var Sequence = sequence.Sequence

// Id returns a Composable that does nothing and returns the Composer as is.
// It is useful as a placeholder or identity operation in functional composition patterns.
func Id() Composable {
return func(c Composer) Composer {
return c
Expand Down
78 changes: 70 additions & 8 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,123 @@ import (
"github.com/zodimo/go-compose/state"
)

// compose-identifier.api.Identifier
// Identifier is a unique identifier for a composable node in the composition tree.
// It is used to track nodes across recompositions.
type Identifier = idApi.Identifier

// type Composition = func(Composable) Composable
// Composable is the fundamental building block of the UI.
// It is a function that takes a Composer and returns a Composer, representing a transformation
// or emission of UI elements into the composition tree.
//
// Unlike Jetpack Compose where @Composable is an annotation, here it is an explicit function signature.
//
// Example:
//
// func MyComponent(c api.Composer) api.Composer {
// return c.Sequence(
// m3Text.Text("Hello"),
// m3Button.Filled(func() { ... }, "Click Me"),
// )
// }
type Composable func(Composer) Composer

// MutableValue is a value holder that can be observed for changes.
// When the value changes, it triggers a recomposition of the scopes that read it.
// This is an alias to state.MutableValue.
type MutableValue = state.MutableValue

// NodePath represents the path to a node in the composition tree.
type NodePath = node.NodePath

// Public API of the composer
// Composer is the interface that orchestrates the composition process.
// It manages the tree of composables, handles state, and builds the final layout tree.
//
// Users interact with the Composer primarily to:
// - Define the structure of the UI using methods like Sequence, If, When.
// - Manage state using Remember and State (via state.SupportState).
// - Apply modifiers to components.
// - Control flow using Key and Range.
type Composer interface {
// --
// GetID returns the unique identifier of the current composable node.
GetID() Identifier

// GetPath returns the path of the current node in the composition tree.
GetPath() NodePath

modifier.ModifierAwareComposer

// -- id management
// GenerateID generates a new unique identifier for a child node.
GenerateID() Identifier

// EmitSlot emits a value into a named slot of the current node.
// This is used for advanced component composition where data needs to be passed
// to the underlying layout node.
EmitSlot(k string, v any) Composer

TreeBuilderComposer
GioLayoutNodeAwareComposer

state.SupportState

// WithComposable executes the given Composable with this Composer.
// It is equivalent to calling composable(c).
WithComposable(composable Composable) Composer

// If conditionally executes one of two Composables based on the boolean condition.
// If condition is true, ifTrue is executed; otherwise, ifFalse is executed.
If(condition bool, ifTrue Composable, ifFalse Composable) Composable

// When conditionally executes a Composable if the boolean condition is true.
// If condition is false, it behaves like an empty composable.
When(condition bool, ifTrue Composable) Composable

// Else conditionally executes a Composable if the boolean condition is false.
// It acts as the inverse of When.
Else(condition bool, ifFalse Composable) Composable

// Sequence executes a list of Composables in order.
// This is the primary way to group multiple components together.
Sequence(contents ...Composable) Composable

// Control Flow
// Key identifies a block of execution with a specific key.
// This is useful for maintaining state when the order of items in a list changes.
Key(key any, content Composable) Composable

// Range loops count times and executes the function fn for each index.
// It is used for rendering lists or repeating elements.
Range(count int, fn func(int) Composable) Composable
}

// Public Modifier interface
// Modifier is an interface for objects that can modify the behavior or appearance of a UI element.
// Modifiers are chained together to apply multiple effects.
type Modifier interface {
// Then chains this modifier with another
// Then chains this modifier with another modifier.
// It returns a new Modifier that represents the combination of the two.
Then(other Modifier) Modifier
}

// LayoutNode represents a node in the layout tree produced by the composition.
// It contains the information needed by the runtime to measure, layout, and draw the UI.
type LayoutNode = layoutnode.LayoutNode

// TreeBuilderComposer provides methods for building the composition tree structure.
// These methods are typically used internally by framework components but are exposed
// for advanced custom component creation.
type TreeBuilderComposer interface {
// StartBlock starts a new group or node in the composition tree with the given key.
StartBlock(key string) Composer

// EndBlock ends the current group or node.
EndBlock() Composer

// Build finalizes the composition and returns the root of the generated layout tree.
Build() LayoutNode
}

// GioLayoutNodeAwareComposer allows setting the widget constructor for the current layout node.
// This bridges the composition world with the underlying Gio widgets.
type GioLayoutNodeAwareComposer interface {
// SetWidgetConstructor sets the function responsible for creating the Gio widget
// associated with the current layout node.
SetWidgetConstructor(constructor layoutnode.LayoutNodeWidgetConstructor)
}
5 changes: 5 additions & 0 deletions runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package runtime

import "gioui.org/op"

// Runtime is the interface responsible for executing the layout tree.
// It bridges the gap between the abstract LayoutNode tree produced by composition
// and the actual drawing operations.
type Runtime interface {
// Run executes the layout logic for the given LayoutNode within the provided LayoutContext.
// It returns an op.CallOp that contains the drawing operations for the node and its children.
Run(LayoutContext, LayoutNode) op.CallOp
}
4 changes: 4 additions & 0 deletions state/memo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import (
"github.com/zodimo/go-compose/internal/immap"
)

// Memo is an alias for an immutable map holding values of any type.
// It is used for memoization and efficient state storage.
type Memo = immap.ImmutableMap[any]

// MemoTyped is an alias for an immutable map holding values of a specific type T.
type MemoTyped[T any] = immap.ImmutableMap[T]

// EmptyMemo returns an empty typed immutable map.
func EmptyMemo[T any]() MemoTyped[T] {
return immap.EmptyImmutableMap[T]()
}
5 changes: 5 additions & 0 deletions state/mutable_value.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package state

// MutableValue is a wrapper around a value that can be read and written.
// Changes to the value are propagated to the composition system to trigger updates.
type MutableValue interface {
// Get retrieves the current value.
Get() any

// Set updates the value and notifies listeners (e.g., the composition system) of the change.
Set(value any)
}
7 changes: 7 additions & 0 deletions state/persistent_state.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package state

// PersistentState is the interface for the underlying store that holds state values.
// It manages the lifecycle of MutableValues and allows the runtime to react to state changes.
type PersistentState interface {
// GetState retrieves or creates a MutableValue for the given key.
// If the state for the key does not exist, it is initialized using the initial function.
GetState(key string, initial func() any) MutableValue

// SetOnStateChange registers a callback that is invoked whenever any state managed by this store changes.
// This is typically used by the runtime to trigger a new frame or recomposition.
SetOnStateChange(callback func())
}
17 changes: 15 additions & 2 deletions state/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
package state

// SupportState defines the interface for state management within a composition.
// It allows composables to remember values and manage state that persists across recompositions.
type SupportState interface {
Remember(key string, calc func() any) any // transient state
State(key string, initial func() any) MutableValue // persistent state
// Remember stores a value computed by calc.
// The value is calculated only once (or when the key changes, if applicable in future implementations)
// and returned on subsequent recompositions.
// This corresponds to transient state that is attached to the current position in the composition.
Remember(key string, calc func() any) any

// State returns a MutableValue that persists across recompositions.
// When the value inside the MutableValue changes, it triggers a recomposition.
//
// Parameters:
// - key: A unique string to identify this state.
// - initial: A function that provides the initial value if the state does not exist.
State(key string, initial func() any) MutableValue
}