core/splitview/splitview.go
Functions
func Children
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
}
Children returns the two panel widgets.