core/splitview/splitview.go

Functions Structs

Functions

func Children

Children returns the two panel widgets.

func (w *Widget) Children() []widget.Widget {
	var children []widget.Widget
	if w.cfg.first != nil {
		children = append(children, w.cfg.first)
	}
	if w.cfg.second != nil {
		children = append(children, w.cfg.second)
	}
	return children
}

func CollapsibleOpt

CollapsibleOpt enables double-click-to-collapse on the divider.

When enabled, double-clicking the divider collapses the first panel.

Double-clicking again restores it to the previous ratio.

func CollapsibleOpt(v bool) Option {
	return func(c *config) { c.collapsible = v }
}

func ColorSchemeOpt

ColorSchemeOpt sets the theme-derived color scheme for the divider.

func ColorSchemeOpt(cs DividerColorScheme) Option {
	return func(c *config) { c.colorScheme = cs }
}

func DividerWidth

DividerWidth sets the divider thickness in pixels.

Default is 6 pixels.

func DividerWidth(w float32) Option {
	return func(c *config) { c.dividerWidth = w }
}

func Draw

Draw renders both panels and the divider.

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

	// Push transform for local coordinate space (children use local bounds).
	canvas.PushTransform(bounds.Min)

	// Draw first panel.
	if w.cfg.first != nil {
		widget.StampScreenOrigin(w.cfg.first, canvas)
		w.cfg.first.Draw(ctx, canvas)
	}

	// Draw divider.
	divRect := w.dividerRect()
	ps := PaintState{
		DividerRect:	divRect,
		Orientation:	w.cfg.orientation,
		Hovered:	w.hovered,
		Dragging:	w.dragging,
		Collapsed:	w.collapsed,
		ColorScheme:	w.cfg.colorScheme,
	}
	w.painter.PaintDivider(canvas, ps)

	// Draw second panel.
	if w.cfg.second != nil {
		widget.StampScreenOrigin(w.cfg.second, canvas)
		w.cfg.second.Draw(ctx, canvas)
	}

	canvas.PopTransform()
}

func Event

Event handles input events for divider dragging.

 

Mouse and wheel event positions are translated from parent-local space

to SplitView-local space before hit-testing and child dispatch, matching

the coordinate convention used by [primitives.BoxWidget].

func (w *Widget) Event(ctx widget.Context, e event.Event) bool {
	// Translate mouse events to local coordinates.
	if me, ok := e.(*event.MouseEvent); ok {
		local := *me
		local.Position = me.Position.Sub(w.Bounds().Min)
		return w.handleMouseEvent(ctx, &local)
	}

	// Translate wheel events to local coordinates.
	if we, ok := e.(*event.WheelEvent); ok {
		local := *we
		local.Position = we.Position.Sub(w.Bounds().Min)
		return w.handleWheelEvent(ctx, &local)
	}

	// Non-positional events (keyboard, focus) broadcast to children.
	if w.cfg.first != nil {
		if w.cfg.first.Event(ctx, e) {
			return true
		}
	}
	if w.cfg.second != nil {
		if w.cfg.second.Event(ctx, e) {
			return true
		}
	}

	return false
}

func First

First sets the first panel widget (left for Horizontal, top for Vertical).

func First(w widget.Widget) Option {
	return func(c *config) { c.first = w }
}

func FirstPanel

FirstPanel returns the first panel widget.

func (w *Widget) FirstPanel() widget.Widget {
	return w.cfg.first
}

func FixedFirst

FixedFirst sets a fixed pixel size for the first panel.

Unlike ratio-based sizing, this keeps the first panel at a constant

pixel width (horizontal) or height (vertical) regardless of window size.

The second panel fills the remaining space.

Set to 0 (default) to use ratio-based sizing.

func FixedFirst(px float32) Option {
	return func(c *config) { c.fixedFirst = px }
}

func InitialRatio

InitialRatio sets the initial split ratio (0.0 to 1.0).

A ratio of 0.3 means the first panel takes 30% of the available space.

Default is 0.5.

func InitialRatio(r float32) Option {
	return func(c *config) { c.ratio = clampRatio(r) }
}

func IsCollapsed

IsCollapsed reports whether the first panel is currently collapsed.

func (w *Widget) IsCollapsed() bool {
	return w.collapsed
}

func Layout

Layout calculates panel sizes and positions children.

func (w *Widget) Layout(ctx widget.Context, constraints geometry.Constraints) geometry.Size {
	size := constraints.Biggest()
	if size.Width <= 0 || size.Height <= 0 {
		fallback := geometry.Sz(defaultViewportWidth, defaultViewportHeight)
		if constraints.MaxWidth > 0 {
			fallback.Width = constraints.MaxWidth
		}
		if constraints.MaxHeight > 0 {
			fallback.Height = constraints.MaxHeight
		}
		size = fallback
	}

	divW := w.cfg.resolvedDividerWidth()

	// If fixedFirst is set, recompute ratio from pixel size each layout pass.
	// This keeps the first panel at a constant pixel size regardless of window size.
	if w.cfg.fixedFirst > 0 {
		totalSpace := size.Width
		if w.cfg.orientation == Vertical {
			totalSpace = size.Height
		}
		totalSpace -= divW
		if totalSpace > 0 {
			w.cfg.ratio = clampRatio(w.cfg.fixedFirst / totalSpace)
		}
	}

	ratio := w.effectiveRatio()

	// Use local origin (0,0) for child placement. The parent positions us
	// via SetBounds after Layout returns, and Draw applies PushTransform
	// to map local coordinates to parent space. Using w.Bounds().Min here
	// is incorrect because Bounds is stale during Layout (parent calls
	// Layout first, then SetBounds).
	origin := geometry.Point{}
	firstRect, secondRect := computePanelRects(size, origin, ratio, divW, w.cfg.orientation)

	w.layoutChild(ctx, w.cfg.first, firstRect)
	w.layoutChild(ctx, w.cfg.second, secondRect)

	return size
}

func MinFirst

MinFirst sets the minimum size (width or height) of the first panel in pixels.

func MinFirst(px float32) Option {
	return func(c *config) { c.minFirst = px }
}

func MinSecond

MinSecond sets the minimum size (width or height) of the second panel in pixels.

func MinSecond(px float32) Option {
	return func(c *config) { c.minSecond = px }
}

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
	}
	if w.cfg.readonlyRatioSignal != nil {
		b := state.BindToScheduler(w.cfg.readonlyRatioSignal, w, sched)
		w.AddBinding(b)
	} else if w.cfg.ratioSignal != nil {
		b := state.BindToScheduler(w.cfg.ratioSignal, w, sched)
		w.AddBinding(b)
	}
}

func New

New creates a new split view Widget with the given options.

 

The returned widget is visible and enabled by default.

The default orientation is [Horizontal] with a 50/50 split.

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

	w.cfg.ratio = defaultRatio

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

	if w.cfg.painter != nil {
		w.painter = w.cfg.painter
	}

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

	return w
}

func OnRatioChange

OnRatioChange sets the callback invoked when the split ratio changes.

func OnRatioChange(fn func(float32)) Option {
	return func(c *config) { c.onRatioChange = fn }
}

func OrientationOpt

OrientationOpt sets the split orientation.

Default is [Horizontal].

func OrientationOpt(o Orientation) Option {
	return func(c *config) { c.orientation = o }
}

func PainterOpt

PainterOpt sets the painter used to render the divider.

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 Ratio

Ratio returns the current split ratio.

func (w *Widget) Ratio() float32 {
	return w.cfg.ResolvedRatio()
}

func RatioReadonlySignal

RatioReadonlySignal binds the split ratio to a read-only signal.

When set, this takes highest precedence over all other ratio sources.

func RatioReadonlySignal(sig state.ReadonlySignal[float32]) Option {
	return func(c *config) { c.readonlyRatioSignal = sig }
}

func RatioSignal

RatioSignal binds the split ratio to a reactive signal.

This is a TWO-WAY binding: the widget reads the ratio from the signal,

and when the user drags the divider, the new ratio is written back.

func RatioSignal(sig state.Signal[float32]) Option {
	return func(c *config) { c.ratioSignal = sig }
}

func ResolvedRatio

ResolvedRatio returns the current split ratio.

Priority: ReadonlySignal > Signal > Static.

func (c *config) ResolvedRatio() float32 {
	if c.readonlyRatioSignal != nil {
		return c.readonlyRatioSignal.Get()
	}
	if c.ratioSignal != nil {
		return c.ratioSignal.Get()
	}
	return c.ratio
}

func Second

Second sets the second panel widget (right for Horizontal, bottom for Vertical).

func Second(w widget.Widget) Option {
	return func(c *config) { c.second = w }
}

func SecondPanel

SecondPanel returns the second panel widget.

func (w *Widget) SecondPanel() widget.Widget {
	return w.cfg.second
}

func String

String returns a human-readable name for the orientation.

func (o Orientation) String() string {
	if int(o) < len(orientationNames) {
		return orientationNames[o]
	}
	return orientationUnknownStr
}

func Unmount

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

Implements [widget.Lifecycle].

func (w *Widget) Unmount() {
	// Bindings are cleaned up automatically by WidgetBase.CleanupBindings().
}

Structs

type Widget struct

Widget implements a resizable split panel container with a draggable divider.

 

A split view is created with [New] using functional options:

 

split := splitview.New(

splitview.First(leftPanel),

splitview.Second(rightPanel),

splitview.OrientationOpt(splitview.Horizontal),

splitview.InitialRatio(0.3),

)

type Widget struct {
	widget.WidgetBase
	cfg	config
	painter	Painter

	// Interaction state.
	hovered		bool
	dragging	bool
	dragStart	geometry.Point
	dragStartRatio	float32

	// Collapse state.
	collapsed	bool
	preCollapse	float32	// ratio before collapse, for restore
	lastClickAt	time.Time
	lastClickPos	geometry.Point
}