core/scrollview/widget.go

Functions Structs

Functions

func Children

Children returns the content widget as the single child.

func (w *Widget) Children() []widget.Widget {
	if w.content == nil {
		return nil
	}
	return []widget.Widget{w.content}
}

func Content

Content returns the scroll view's content widget.

func (w *Widget) Content() widget.Widget {
	return w.content
}

func ContentSize

ContentSize returns the measured content size.

func (w *Widget) ContentSize() geometry.Size {
	return w.contentSize
}

func Draw

Draw renders the scroll view to the canvas.

 

Drawing order:

1. Push clip to viewport bounds

2. Push transform for scroll offset

3. Draw content

4. Pop transform

5. Pop clip

6. Draw scrollbar(s) on top

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

	// Process track repeat (page-scroll while mouse held on track).
	w.tickTrackRepeat(ctx)

	scrollX := w.cfg.ResolvedScrollX()
	scrollY := w.cfg.ResolvedScrollY()

	// Clip content to viewport.
	canvas.PushClip(bounds)
	canvas.PushTransform(geometry.Pt(bounds.Min.X-scrollX, bounds.Min.Y-scrollY))

	if w.content != nil {
		widget.StampScreenOrigin(w.content, canvas)
		w.content.Draw(ctx, canvas)
	}

	canvas.PopTransform()
	canvas.PopClip()

	// Draw scrollbar(s) on top.
	w.paintScrollbars(canvas)
}

func Event

Event handles an input event and returns true if consumed.

 

Event coordinates are transformed from parent space to content space before

dispatching to the content widget. This is the inverse of the Draw transform

(PushTransform with scroll offset), following the universal GUI framework

pattern used by Flutter, Qt, WPF, and Gio.

 

Scrollbar interactions are NOT transformed because scrollbars are drawn

on top of the viewport in parent-space coordinates.

func (w *Widget) Event(ctx widget.Context, e event.Event) bool {
	// Handle scrollbar interactions FIRST so the thumb drag always works,
	// even when content would consume the same mouse event (e.g., item click).
	if me, ok := e.(*event.MouseEvent); ok {
		if w.dragging != dragNone || w.isOnScrollbar(me.Position) {
			return handleEvent(w, ctx, e)
		}
	}

	// Only dispatch to content if the event position is within the viewport.
	// This prevents content from receiving events when the mouse is outside.
	// Skip this check if bounds haven't been set yet (empty rect).
	if !w.isEventInsideViewport(e) {
		return handleEvent(w, ctx, e)
	}

	// Transform event coordinates to content space and dispatch to content.
	if w.content != nil {
		contentEvent := w.transformToContentSpace(e)
		if consumed := w.content.Event(ctx, contentEvent); consumed {
			return true
		}
	}

	return handleEvent(w, ctx, e)
}

func IsFocusable

IsFocusable reports whether the scroll view can currently receive focus.

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

func IsViewportClip

IsViewportClip tells the dirty Collector that this widget acts as a

viewport boundary (Flutter RenderViewport pattern). The Collector adds

this widget's own bounds as the dirty region and does NOT recurse into

children — scroll content may have bounds exceeding the viewport.

func (w *Widget) IsViewportClip() bool	{ return true }

func Layout

Layout calculates the scroll view's size and measures its content.

 

The viewport is constrained to the parent's constraints. The content

is measured with unconstrained dimensions along the scroll axis to

determine its natural size.

func (w *Widget) Layout(ctx widget.Context, constraints geometry.Constraints) geometry.Size {
	// The viewport fills the available space.
	w.viewportSize = constraints.Biggest()
	if w.viewportSize.Width <= 0 || w.viewportSize.Height <= 0 {
		w.viewportSize = constraints.Constrain(geometry.Sz(defaultViewportWidth, defaultViewportHeight))
	}

	if w.content == nil {
		return w.viewportSize
	}

	// Build content constraints: unconstrained along scroll axes.
	contentConstraints := w.buildContentConstraints()

	// Measure content.
	w.contentSize = w.content.Layout(ctx, contentConstraints)

	// Set content bounds at (0, 0) with its natural size.
	if setter, ok := w.content.(interface{ SetBounds(geometry.Rect) }); ok {
		setter.SetBounds(geometry.NewRect(0, 0, w.contentSize.Width, w.contentSize.Height))
	}

	return w.viewportSize
}

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

func New

New creates a new scroll view Widget wrapping the given content widget.

 

The returned widget is visible, enabled, and focusable by default.

The default direction is [Vertical] with [ScrollbarAuto] visibility.

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

	// Set parent so dirty propagation and viewport clipping work correctly.
	// Android pattern: invalidateChildInParent() clips dirty rect to parent bounds.
	// Without this, content.Parent()=nil and clipToParentViewport cannot clip
	// content bounds (e.g. 36000px) to viewport bounds.
	if setter, ok := content.(interface{ SetParent(widget.Widget) }); ok {
		setter.SetParent(w)
	}

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

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

	return w
}

func Padding

Padding sets the content padding. Returns the widget for method chaining.

func (w *Widget) Padding(_ float32) *Widget {
	// Reserved for future use. Currently a no-op.
	return w
}

func ScrollOffset

ScrollOffset returns the current scroll offset.

func (w *Widget) ScrollOffset() (x, y float32) {
	return w.cfg.ResolvedScrollX(), w.cfg.ResolvedScrollY()
}

func ScrollbarInset

ScrollbarInset returns the width reserved for visible scrollbars.

When the vertical scrollbar is shown, this is the total scrollbar width

(track + padding). Otherwise zero.

func (w *Widget) ScrollbarInset() float32 {
	if w.shouldShowVScrollbar() {
		return scrollbarWidth + scrollbarPadding*2
	}
	return 0
}

func Unmount

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

Implements [widget.Lifecycle].

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

func ViewportSize

ViewportSize returns the current viewport size.

func (w *Widget) ViewportSize() geometry.Size {
	return w.viewportSize
}

Structs

type Widget struct

Widget implements a scrollable container that clips and translates its

content child widget.

 

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

 

sv := scrollview.New(content,

scrollview.DirectionOpt(scrollview.Vertical),

scrollview.OnScroll(handleScroll),

)

type Widget struct {
	widget.WidgetBase
	cfg	config
	content	widget.Widget
	painter	Painter

	// Cached layout measurements.
	contentSize	geometry.Size
	viewportSize	geometry.Size

	// Interaction state.
	hovered		bool
	dragging	dragAxis
	dragStart	geometry.Point
	dragScrollStart	float32

	// Track repeat state: page-scroll repeats while mouse is held on the track.
	trackRepeat	trackRepeatState
}