core/collapsible/collapsible.go

Functions Structs

Functions

func Children

Children returns the content widget for tree traversal.

The content is always returned even when collapsed, to allow the framework

to manage lifecycle and focus traversal.

func (w *Widget) Children() []widget.Widget {
	children := make([]widget.Widget, 0, 2)
	if w.headerTitle != nil {
		children = append(children, w.headerTitle)
	}
	if w.cfg.content != nil {
		children = append(children, w.cfg.content)
	}
	if len(children) == 0 {
		return nil
	}
	return children
}

func Draw

Draw renders the collapsible section.

func (w *Widget) Draw(ctx widget.Context, canvas widget.Canvas) {
	bounds := w.Bounds()
	if bounds.IsEmpty() {
		return
	}

	headerBounds := geometry.NewRect(
		bounds.Min.X, bounds.Min.Y,
		bounds.Width(), w.cfg.headerHeight,
	)

	// Set bounds and stamp screen origin on header title widget for dirty tracking.
	if w.headerTitle != nil {
		if setter, ok := w.headerTitle.(interface{ SetBounds(geometry.Rect) }); ok {
			setter.SetBounds(headerBounds)
		}
		widget.StampScreenOrigin(w.headerTitle, canvas)
	}

	// Paint header via the painter.
	w.painter.PaintHeader(canvas, HeaderState{
		Title:		w.cfg.ResolvedTitle(),
		Expanded:	w.cfg.ResolvedExpanded(),
		Hovered:	w.istate == stateHover,
		Pressed:	w.istate == statePressed,
		Focused:	w.IsFocused(),
		Bounds:		headerBounds,
		ArrowProgress:	w.progress,
		HeaderColor:	w.cfg.headerColor,
		ArrowColor:	w.cfg.arrowColor,
	})

	// Draw content if visible (progress > 0).
	if w.progress > 0 && w.cfg.content != nil {
		w.drawContent(ctx, canvas, bounds)
	} else if w.cfg.content != nil {
		// Collapsed (progress == 0): stamp an empty clip on content so child
		// boundaries receive a zero-size CompositorClip. This causes
		// isBoundaryLayerVisible to cull them, preventing stale textures
		// from being blitted at the old position (#122).
		emptyClip := geometry.Rect{}
		canvas.PushClip(emptyClip)
		widget.DrawChild(w.cfg.content, ctx, canvas)
		canvas.PopClip()
	}
}

func Event

Event handles input events and returns true if consumed.

func (w *Widget) Event(ctx widget.Context, e event.Event) bool {
	// Let header handle its own events first.
	if handleEvent(w, ctx, e) {
		return true
	}

	// Forward to content widget when expanded.
	if w.IsExpanded() && w.cfg.content != nil {
		return w.cfg.content.Event(ctx, e)
	}

	return false
}

func Get

Get returns the current progress.

func (a *progressAdapter) Get() float32 {
	return a.w.progress
}

func IsAnimating

IsAnimating reports whether an expand/collapse animation is in progress.

func (w *Widget) IsAnimating() bool {
	return w.animCtrl != nil && w.animCtrl.HasActive()
}

func IsExpanded

IsExpanded returns the current expanded state.

func (w *Widget) IsExpanded() bool {
	return w.cfg.ResolvedExpanded()
}

func IsFocusable

IsFocusable reports whether the collapsible section can receive focus.

func (w *Widget) IsFocusable() bool {
	return w.IsVisible() && w.IsEnabled()
}

func Layout

Layout calculates the widget's size given constraints.

 

The height depends on the animation progress:

- Collapsed: headerHeight only

- Expanded: headerHeight + content height

- Animating: headerHeight + content height * progress

func (w *Widget) Layout(ctx widget.Context, constraints geometry.Constraints) geometry.Size {
	// Tick animation if active.
	w.tickAnimation(ctx)

	headerH := w.cfg.headerHeight

	// Layout content to determine its natural height.
	var contentH float32
	if w.cfg.content != nil {
		maxContentH := constraints.MaxHeight - headerH
		if maxContentH < 0 {
			maxContentH = 0
		}
		contentConstraints := geometry.BoxConstraints(
			constraints.MinWidth, constraints.MaxWidth,
			0, maxContentH,
		)
		w.contentSize = w.cfg.content.Layout(ctx, contentConstraints)
		contentH = w.contentSize.Height
	}

	// Total height = header + animated content portion.
	totalH := headerH + contentH*w.progress
	preferred := geometry.Sz(constraints.MaxWidth, totalH)
	return constraints.Constrain(preferred)
}

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 title signals to HEADER widget (not self) so dirty.Collector
	// reports header bounds, not full collapsible bounds.
	titleTarget := w.headerTitle
	if titleTarget == nil {
		titleTarget = w
	}
	if w.cfg.readonlyTitleSignal != nil {
		b := state.BindToScheduler(w.cfg.readonlyTitleSignal, titleTarget, sched)
		w.AddBinding(b)
	} else if w.cfg.titleSignal != nil {
		b := state.BindToScheduler(w.cfg.titleSignal, titleTarget, sched)
		w.AddBinding(b)
	}
	if w.cfg.readonlyExpandedSignal != nil {
		b := state.BindToScheduler(w.cfg.readonlyExpandedSignal, w, sched)
		w.AddBinding(b)
	} else if w.cfg.expandedSignal != nil {
		b := state.BindToScheduler(w.cfg.expandedSignal, w, sched)
		w.AddBinding(b)
	}
}

func New

New creates a new collapsible section Widget with the given options.

 

The returned widget is visible and enabled by default.

The content starts collapsed unless [Expanded] is set to true.

func New(opts ...Option) *Widget {
	w := &Widget{
		painter:	DefaultPainter{},
		animCtrl:	animation.NewController(),
	}
	w.SetVisible(true)
	w.SetEnabled(true)

	// Set defaults before applying options.
	w.cfg.headerHeight = defaultHeaderHeight
	w.cfg.animated = true
	w.cfg.duration = defaultAnimDuration

	for _, opt := range opts {
		opt(&w.cfg)
	}

	// Apply painter from config if set.
	if w.cfg.painter != nil {
		w.painter = w.cfg.painter
	}

	// Initialize progress based on expanded state.
	if w.cfg.ResolvedExpanded() {
		w.progress = 1.0
	}

	// Create internal header title widget for dirty tracking.
	w.headerTitle = newHeaderTextWidget()

	// ADR-028: parent chain for upward dirty propagation.
	// Flutter: RenderObject.adoptChild sets parent on each child.
	type parentSetter interface{ SetParent(widget.Widget) }
	if ps, ok := w.headerTitle.(parentSetter); ok {
		ps.SetParent(w)
	}
	if w.cfg.content != nil {
		if ps, ok := w.cfg.content.(parentSetter); ok {
			ps.SetParent(w)
		}
	}

	return w
}

func Progress

Progress returns the current animation progress (0.0 = collapsed, 1.0 = expanded).

This is useful for testing and external monitoring.

func (w *Widget) Progress() float32 {
	return w.progress
}

func Set

Set updates the progress and marks the widget for redraw.

During collapse animation, the clip area shrinks each frame.

InvalidateScene ensures the enclosing boundary's GPU texture is

re-recorded so stale pixels outside the new clip are cleared.

func (a *progressAdapter) Set(v float32) {
	a.w.progress = v
	a.w.SetNeedsRedraw(true)
	a.w.InvalidateScene()
}

func SetExpanded

SetExpanded sets the expanded state programmatically.

If animated, triggers a smooth transition.

func (w *Widget) SetExpanded(expanded bool) {
	current := w.cfg.ResolvedExpanded()
	if current == expanded {
		return
	}
	w.setExpandedState(expanded)
}

func Toggle

Toggle toggles the expanded state.

func (w *Widget) Toggle() {
	w.setExpandedState(!w.cfg.ResolvedExpanded())
}

func Unmount

Unmount is called when the widget is removed from the tree.

Implements [widget.Lifecycle].

func (w *Widget) Unmount() {
	// Cancel any running animation.
	if w.animCtrl != nil {
		w.animCtrl.CancelAll()
	}
}

Structs

type Widget struct

Widget implements a collapsible section with a clickable header and

expandable content area.

 

Create with [New] using functional options:

 

section := collapsible.New(

collapsible.Title("Details"),

collapsible.Content(detailWidget),

collapsible.Expanded(true),

)

type Widget struct {
	widget.WidgetBase
	cfg	config
	istate	interactionState
	painter	Painter

	// Animation state.
	progress	float32	// 0.0 = collapsed, 1.0 = expanded
	animCtrl	*animation.Controller

	// Cached content size from last layout.
	contentSize	geometry.Size

	// headerTitle is an internal TextWidget for the header title text.
	// It participates in Children() so dirty.Collector can track title
	// changes independently (e.g., TitleSignal updates → cyan overlay).
	headerTitle	widget.Widget
}