~eliasnaur/gio

2782436ffc1348249baa41f54229b1826fed4e80 — Elias Naur 4 years ago b928ee6
ui/layout: add common state to Context

Almost every layout and widget need the ui.Config for its environment,
an ui.Ops to store operations. Stateful widgets need an input.Queue
for events.

Add all these common objects to Context, greatly simplifying the
function signatures for Gio programs.

Fixes gio#33

Signed-off-by: Elias Naur <mail@eliasnaur.com>
M ui/layout/doc.go => ui/layout/doc.go +7 -7
@@ 13,17 13,17 @@ in an implicit Context to keep the Widget declaration short.

For example, to add space above a widget:

	ctx := new(layout.Context)
	ctx.Constraints = ...
	c := &layout.Context{...}
	c.Reset(...)

	// Configure a top inset.
	inset := layout.Inset{Top: ui.Dp(8), ...}
	// Use the inset to lay out a widget.
	inset.Layout(..., ctx, func() {
	inset.Layout(c, func() {
		// Lay out widget and determine its size given the constraints.
		...
		dims := layout.Dimensions{...}
		ctx.Dimensions = dims
		c.Dimensions = dims
	})

Note that the example does not generate any garbage even though the


@@ 37,10 37,10 @@ be created from a few generic layouts.
This example both aligns and insets a child:

	inset := layout.Inset{...}
	inset.Layout(..., ctx, func() {
	inset.Layout(c, func() {
		align := layout.Align(...)
		align.Layout(..., ctx, func() {
			widget.Layout(..., ctx)
		align.Layout(c, func() {
			widget.Layout(c, ...)
		})
	})


M ui/layout/flex.go => ui/layout/flex.go +6 -8
@@ 22,7 22,6 @@ type Flex struct {

	ctx       *Context
	macro     ui.MacroOp
	ops       *ui.Ops
	mode      flexMode
	size      int
	rigidSize int


@@ 69,13 68,12 @@ const (
)

// Init must be called before Rigid or Flexible.
func (f *Flex) Init(ops *ui.Ops, ctx *Context) *Flex {
func (f *Flex) Init(c *Context) *Flex {
	if f.mode > modeBegun {
		panic("must End the current child before calling Init again")
	}
	f.mode = modeBegun
	f.ops = ops
	f.ctx = ctx
	f.ctx = c
	f.size = 0
	f.rigidSize = 0
	f.maxCross = 0


@@ 91,7 89,7 @@ func (f *Flex) begin(mode flexMode) {
		panic("must End before adding a child")
	}
	f.mode = mode
	f.macro.Record(f.ops)
	f.macro.Record(f.ctx.Ops)
}

// Rigid lays out a widget with the main axis constrained to the range


@@ 192,9 190,9 @@ func (f *Flex) Layout(children ...FlexChild) {
			}
		}
		var stack ui.StackOp
		stack.Push(f.ops)
		ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ops)
		child.macro.Add(f.ops)
		stack.Push(f.ctx.Ops)
		ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ctx.Ops)
		child.macro.Add(f.ctx.Ops)
		stack.Pop()
		mainSize += axisMain(f.Axis, dims.Size)
		if i < len(children)-1 {

M ui/layout/layout.go => ui/layout/layout.go +36 -18
@@ 6,6 6,7 @@ import (
	"image"

	"gioui.org/ui"
	"gioui.org/ui/input"
)

// Constraints represent a set of acceptable ranges for


@@ 41,11 42,18 @@ type Direction uint8
// computing dimensions for a user interface element.
type Widget func()

// Context tracks the current constraints and dimensions during
// layout.
// Context carry the state needed by almost all layouts and widgets.
type Context struct {
	// Constraints track the constraints for the active widget or
	// layout.
	Constraints Constraints
	Dimensions  Dimensions
	// Dimensions track the result of the most recent layout
	// operation.
	Dimensions Dimensions

	ui.Config
	input.Queue
	*ui.Ops
}

const (


@@ 83,6 91,16 @@ func (s *Context) Layout(cs Constraints, w Widget) Dimensions {
	return s.Dimensions
}

// Reset the context.
func (c *Context) Reset(cfg ui.Config, cs Constraints) {
	c.Constraints = cs
	c.Config = cfg
	if c.Ops == nil {
		c.Ops = new(ui.Ops)
	}
	c.Ops.Reset()
}

// Constrain a value to the range [Min; Max].
func (c Constraint) Constrain(v int) int {
	if v < c.Min {


@@ 116,12 134,12 @@ type Inset struct {
type Align Direction

// Layout a widget.
func (in Inset) Layout(c ui.Config, ops *ui.Ops, ctx *Context, w Widget) {
func (in Inset) Layout(c *Context, w Widget) {
	top := c.Px(in.Top)
	right := c.Px(in.Right)
	bottom := c.Px(in.Bottom)
	left := c.Px(in.Left)
	mcs := ctx.Constraints
	mcs := c.Constraints
	mcs.Width.Min -= left + right
	mcs.Width.Max -= left + right
	if mcs.Width.Min < 0 {


@@ 139,12 157,12 @@ func (in Inset) Layout(c ui.Config, ops *ui.Ops, ctx *Context, w Widget) {
		mcs.Height.Max = mcs.Height.Min
	}
	var stack ui.StackOp
	stack.Push(ops)
	ui.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(ops)
	dims := ctx.Layout(mcs, w)
	stack.Push(c.Ops)
	ui.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(c.Ops)
	dims := c.Layout(mcs, w)
	stack.Pop()
	ctx.Dimensions = Dimensions{
		Size:     ctx.Constraints.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})),
	c.Dimensions = Dimensions{
		Size:     c.Constraints.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})),
		Baseline: dims.Baseline + top,
	}
}


@@ 156,14 174,14 @@ func UniformInset(v ui.Value) Inset {
}

// Layout a widget.
func (a Align) Layout(ops *ui.Ops, st *Context, w Widget) {
func (a Align) Layout(c *Context, w Widget) {
	var macro ui.MacroOp
	macro.Record(ops)
	cs := st.Constraints
	macro.Record(c.Ops)
	cs := c.Constraints
	mcs := cs
	mcs.Width.Min = 0
	mcs.Height.Min = 0
	dims := st.Layout(mcs, w)
	dims := c.Layout(mcs, w)
	macro.Stop()
	sz := dims.Size
	if sz.X < cs.Width.Min {


@@ 186,11 204,11 @@ func (a Align) Layout(ops *ui.Ops, st *Context, w Widget) {
		p.Y = sz.Y - dims.Size.Y
	}
	var stack ui.StackOp
	stack.Push(ops)
	ui.TransformOp{}.Offset(toPointF(p)).Add(ops)
	macro.Add(ops)
	stack.Push(c.Ops)
	ui.TransformOp{}.Offset(toPointF(p)).Add(c.Ops)
	macro.Add(c.Ops)
	stack.Pop()
	st.Dimensions = Dimensions{
	c.Dimensions = Dimensions{
		Size:     sz,
		Baseline: dims.Baseline,
	}

M ui/layout/layout_test.go => ui/layout/layout_test.go +36 -40
@@ 18,22 18,22 @@ var q queue
var cfg = new(config)

func ExampleInset() {
	ops := new(ui.Ops)
	ctx := new(layout.Context)

	c := &layout.Context{Queue: q}
	// Loose constraints with no minimal size.
	ctx.Constraints.Width.Max = 100
	ctx.Constraints.Height.Max = 100
	var cs layout.Constraints
	cs.Width.Max = 100
	cs.Height.Max = 100
	c.Reset(cfg, cs)

	// Inset all edges by 10.
	inset := layout.UniformInset(ui.Dp(10))
	inset.Layout(cfg, ops, ctx, func() {
	inset.Layout(c, func() {
		// Lay out a 50x50 sized widget.
		layoutWidget(ctx, 50, 50)
		fmt.Println(ctx.Dimensions.Size)
		layoutWidget(c, 50, 50)
		fmt.Println(c.Dimensions.Size)
	})

	fmt.Println(ctx.Dimensions.Size)
	fmt.Println(c.Dimensions.Size)

	// Output:
	// (50,50)


@@ 41,20 41,19 @@ func ExampleInset() {
}

func ExampleAlign() {
	ops := new(ui.Ops)
	ctx := new(layout.Context)

	c := &layout.Context{Queue: q}
	// Rigid constraints with both minimum and maximum set.
	ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
	cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c.Reset(cfg, cs)

	align := layout.Align(layout.Center)
	align.Layout(ops, ctx, func() {
	align.Layout(c, func() {
		// Lay out a 50x50 sized widget.
		layoutWidget(ctx, 50, 50)
		fmt.Println(ctx.Dimensions.Size)
		layoutWidget(c, 50, 50)
		fmt.Println(c.Dimensions.Size)
	})

	fmt.Println(ctx.Dimensions.Size)
	fmt.Println(c.Dimensions.Size)

	// Output:
	// (50,50)


@@ 62,24 61,23 @@ func ExampleAlign() {
}

func ExampleFlex() {
	ops := new(ui.Ops)
	ctx := new(layout.Context)

	ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c := &layout.Context{Queue: q}
	cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c.Reset(cfg, cs)

	flex := layout.Flex{}
	flex.Init(ops, ctx)
	flex.Init(c)

	// Rigid 10x10 widget.
	child1 := flex.Rigid(func() {
		fmt.Printf("Rigid: %v\n", ctx.Constraints.Width)
		layoutWidget(ctx, 10, 10)
		fmt.Printf("Rigid: %v\n", c.Constraints.Width)
		layoutWidget(c, 10, 10)
	})

	// Child with 50% space allowance.
	child2 := flex.Flexible(0.5, func() {
		fmt.Printf("50%%: %v\n", ctx.Constraints.Width)
		layoutWidget(ctx, 10, 10)
		fmt.Printf("50%%: %v\n", c.Constraints.Width)
		layoutWidget(c, 10, 10)
	})

	flex.Layout(child1, child2)


@@ 90,23 88,22 @@ func ExampleFlex() {
}

func ExampleStack() {
	ops := new(ui.Ops)
	ctx := new(layout.Context)

	ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c := &layout.Context{Queue: q}
	cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c.Reset(cfg, cs)

	stack := layout.Stack{}
	stack.Init(ops, ctx)
	stack.Init(c)

	// Rigid 50x50 widget.
	child1 := stack.Rigid(func() {
		layoutWidget(ctx, 50, 50)
		layoutWidget(c, 50, 50)
	})

	// Force widget to the same size as the first.
	child2 := stack.Expand(func() {
		fmt.Printf("Expand: %v\n", ctx.Constraints)
		layoutWidget(ctx, 10, 10)
		fmt.Printf("Expand: %v\n", c.Constraints)
		layoutWidget(c, 10, 10)
	})

	stack.Layout(child1, child2)


@@ 116,19 113,18 @@ func ExampleStack() {
}

func ExampleList() {
	ops := new(ui.Ops)
	ctx := new(layout.Context)

	ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c := &layout.Context{Queue: q}
	cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
	c.Reset(cfg, cs)

	// The list is 1e6 elements, but only 5 fit the constraints.
	const listLen = 1e6

	var list layout.List
	count := 0
	list.Layout(cfg, q, ops, ctx, listLen, func(i int) {
	list.Layout(c, listLen, func(i int) {
		count++
		layoutWidget(ctx, 20, 20)
		layoutWidget(c, 20, 20)
	})

	fmt.Println(count)

M ui/layout/list.go => ui/layout/list.go +15 -23
@@ 7,7 7,6 @@ import (

	"gioui.org/ui"
	"gioui.org/ui/gesture"
	"gioui.org/ui/input"
	"gioui.org/ui/paint"
	"gioui.org/ui/pointer"
)


@@ 33,9 32,7 @@ type List struct {
	// the very end.
	beforeEnd bool

	config      ui.Config
	ops         *ui.Ops
	queue       input.Queue
	ctx         *Context
	macro       ui.MacroOp
	child       ui.MacroOp
	scroll      gesture.Scroll


@@ 47,7 44,6 @@ type List struct {
	// to the child with index first.
	offset int

	cs  Constraints
	len int

	// maxSize is the total size of visible children.


@@ 71,18 67,15 @@ const (
const inf = 1e6

// Init prepares the list for iterating through its children with Next.
func (l *List) init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int) {
func (l *List) init(c *Context, len int) {
	if l.more() {
		panic("unfinished child")
	}
	l.config = cfg
	l.queue = q
	l.update()
	l.ops = ops
	l.ctx = c
	l.maxSize = 0
	l.children = l.children[:0]
	l.cs = cs
	l.len = len
	l.update()
	if l.scrollToEnd() {
		l.offset = 0
		l.first = len


@@ 91,21 84,20 @@ func (l *List) init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, l
		l.offset = 0
		l.first = len
	}
	l.macro.Record(ops)
	l.macro.Record(c.Ops)
	l.next()
}

// Layout the List and return its dimensions.
func (l *List) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *Context, len int, w ListElement) {
	cs := ctx.Constraints
	for l.init(c, q, ops, cs, len); l.more(); l.next() {
		cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.cs))
func (l *List) Layout(c *Context, len int, w ListElement) {
	for l.init(c, len); l.more(); l.next() {
		cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints))
		i := l.index()
		l.end(ctx.Layout(cs, func() {
		l.end(c.Layout(cs, func() {
			w(i)
		}))
	}
	ctx.Dimensions = l.layout()
	c.Dimensions = l.layout()
}

func (l *List) scrollToEnd() bool {


@@ 118,7 110,7 @@ func (l *List) Dragging() bool {
}

func (l *List) update() {
	d := l.scroll.Scroll(l.config, l.queue, gesture.Axis(l.Axis))
	d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, gesture.Axis(l.Axis))
	l.scrollDelta = d
	l.offset += d
}


@@ 134,7 126,7 @@ func (l *List) next() {
		l.dir = l.nextDir()
	}
	if l.more() {
		l.child.Record(l.ops)
		l.child.Record(l.ctx.Ops)
	}
}



@@ 156,7 148,7 @@ func (l *List) more() bool {
}

func (l *List) nextDir() iterationDir {
	vsize := axisMainConstraint(l.Axis, l.cs).Max
	vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max
	last := l.first + len(l.children)
	// Clamp offset.
	if l.maxSize-l.offset < vsize && last == l.len {


@@ 200,7 192,7 @@ func (l *List) layout() Dimensions {
	if l.more() {
		panic("unfinished child")
	}
	mainc := axisMainConstraint(l.Axis, l.cs)
	mainc := axisMainConstraint(l.Axis, l.ctx.Constraints)
	children := l.children
	// Skip invisible children
	for len(children) > 0 {


@@ 226,7 218,7 @@ func (l *List) layout() Dimensions {
			break
		}
	}
	ops := l.ops
	ops := l.ctx.Ops
	pos := -l.offset
	// ScrollToEnd lists lists are end aligned.
	if space := mainc.Max - size; l.ScrollToEnd && space > 0 {

M ui/layout/stack.go => ui/layout/stack.go +6 -8
@@ 16,7 16,6 @@ type Stack struct {
	Alignment Direction

	macro       ui.MacroOp
	ops         *ui.Ops
	constrained bool
	ctx         *Context
	maxSZ       image.Point


@@ 30,9 29,8 @@ type StackChild struct {
}

// Init a stack before calling Rigid or Expand.
func (s *Stack) Init(ops *ui.Ops, ctx *Context) *Stack {
	s.ops = ops
	s.ctx = ctx
func (s *Stack) Init(c *Context) *Stack {
	s.ctx = c
	s.constrained = true
	s.maxSZ = image.Point{}
	s.baseline = 0


@@ 43,7 41,7 @@ func (s *Stack) begin() {
	if !s.constrained {
		panic("must Init before adding a child")
	}
	s.macro.Record(s.ops)
	s.macro.Record(s.ctx.Ops)
}

// Rigid lays out a widget with the same constraints that were


@@ 102,9 100,9 @@ func (s *Stack) Layout(children ...StackChild) {
			p.Y = s.maxSZ.Y - sz.Y
		}
		var stack ui.StackOp
		stack.Push(s.ops)
		ui.TransformOp{}.Offset(toPointF(p)).Add(s.ops)
		ch.macro.Add(s.ops)
		stack.Push(s.ctx.Ops)
		ui.TransformOp{}.Offset(toPointF(p)).Add(s.ctx.Ops)
		ch.macro.Add(s.ctx.Ops)
		stack.Pop()
	}
	b := s.baseline

M ui/text/editor.go => ui/text/editor.go +37 -38
@@ 11,7 11,6 @@ import (

	"gioui.org/ui"
	"gioui.org/ui/gesture"
	"gioui.org/ui/input"
	"gioui.org/ui/key"
	"gioui.org/ui/layout"
	"gioui.org/ui/paint"


@@ 82,9 81,9 @@ const (
)

// Next returns the next available editor event, or false if none are available.
func (e *Editor) Next(cfg ui.Config, queue input.Queue) (EditorEvent, bool) {
func (e *Editor) Next(c *layout.Context) (EditorEvent, bool) {
	// Crude configuration change detection.
	if scale := cfg.Px(ui.Sp(100)); scale != e.oldScale {
	if scale := c.Px(ui.Sp(100)); scale != e.oldScale {
		e.invalidate()
		e.oldScale = scale
	}


@@ 98,7 97,7 @@ func (e *Editor) Next(cfg ui.Config, queue input.Queue) (EditorEvent, bool) {
		axis = gesture.Vertical
		smin, smax = sbounds.Min.Y, sbounds.Max.Y
	}
	sdist := e.scroller.Scroll(cfg, queue, axis)
	sdist := e.scroller.Scroll(c.Config, c.Queue, axis)
	var soff int
	if e.SingleLine {
		e.scrollOff.X += sdist


@@ 107,26 106,26 @@ func (e *Editor) Next(cfg ui.Config, queue input.Queue) (EditorEvent, bool) {
		e.scrollOff.Y += sdist
		soff = e.scrollOff.Y
	}
	for evt, ok := e.clicker.Next(queue); ok; evt, ok = e.clicker.Next(queue) {
	for evt, ok := e.clicker.Next(c.Queue); ok; evt, ok = e.clicker.Next(c.Queue) {
		switch {
		case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
			evt.Type == gesture.TypeClick && evt.Source == pointer.Touch:
			e.blinkStart = cfg.Now()
			e.blinkStart = c.Now()
			e.moveCoord(image.Point{
				X: int(math.Round(float64(evt.Position.X))),
				Y: int(math.Round(float64(evt.Position.Y))),
			})
			e.requestFocus = true
			if e.scroller.State() != gesture.StateFlinging {
				e.scrollToCaret(cfg)
				e.scrollToCaret(c)
			}
		}
	}
	if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
		e.scroller.Stop()
	}
	for ke, ok := queue.Next(e); ok; ke, ok = queue.Next(e) {
		e.blinkStart = cfg.Now()
	for ke, ok := c.Queue.Next(e); ok; ke, ok = c.Queue.Next(e) {
		e.blinkStart = c.Now()
		switch ke := ke.(type) {
		case key.FocusEvent:
			e.focused = ke.Focus


@@ 140,11 139,11 @@ func (e *Editor) Next(cfg ui.Config, queue input.Queue) (EditorEvent, bool) {
				}
			}
			if e.command(ke) {
				e.scrollToCaret(cfg)
				e.scrollToCaret(c.Config)
				e.scroller.Stop()
			}
		case key.EditEvent:
			e.scrollToCaret(cfg)
			e.scrollToCaret(c)
			e.scroller.Stop()
			e.append(ke.Text)
		}


@@ 165,11 164,11 @@ func (e *Editor) Focus() {
	e.requestFocus = true
}

func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, ctx *layout.Context) {
	cs := ctx.Constraints
	for _, ok := e.Next(cfg, queue); ok; _, ok = e.Next(cfg, queue) {
func (e *Editor) Layout(c *layout.Context) {
	cs := c.Constraints
	for _, ok := e.Next(c); ok; _, ok = e.Next(c) {
	}
	twoDp := cfg.Px(ui.Dp(2))
	twoDp := c.Px(ui.Dp(2))
	e.padLeft, e.padRight = twoDp, twoDp
	maxWidth := cs.Width.Max
	if e.SingleLine {


@@ 197,7 196,7 @@ func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, ctx *layo
		Min: image.Point{X: 0, Y: 0},
		Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y},
	}
	key.InputOp{Key: e, Focus: e.requestFocus}.Add(ops)
	key.InputOp{Key: e, Focus: e.requestFocus}.Add(c.Ops)
	e.requestFocus = false
	e.it = lineIterator{
		Lines:     lines,


@@ 207,14 206,14 @@ func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, ctx *layo
		Offset:    off,
	}
	var stack ui.StackOp
	stack.Push(ops)
	stack.Push(c.Ops)
	// Apply material. Set a default color in case the material is empty.
	if e.rr.len() > 0 {
		paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(ops)
		e.Material.Add(ops)
		paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(c.Ops)
		e.Material.Add(c.Ops)
	} else {
		paint.ColorOp{Color: color.RGBA{A: 0xaa}}.Add(ops)
		e.HintMaterial.Add(ops)
		paint.ColorOp{Color: color.RGBA{A: 0xaa}}.Add(c.Ops)
		e.HintMaterial.Add(c.Ops)
	}
	for {
		str, lineOff, ok := e.it.Next()


@@ 222,21 221,21 @@ func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, ctx *layo
			break
		}
		var stack ui.StackOp
		stack.Push(ops)
		ui.TransformOp{}.Offset(lineOff).Add(ops)
		e.Face.Path(str).Add(ops)
		paint.PaintOp{Rect: toRectF(clip).Sub(lineOff)}.Add(ops)
		stack.Push(c.Ops)
		ui.TransformOp{}.Offset(lineOff).Add(c.Ops)
		e.Face.Path(str).Add(c.Ops)
		paint.PaintOp{Rect: toRectF(clip).Sub(lineOff)}.Add(c.Ops)
		stack.Pop()
	}
	if e.focused {
		now := cfg.Now()
		now := c.Now()
		dt := now.Sub(e.blinkStart)
		blinking := dt < maxBlinkDuration
		const timePerBlink = time.Second / blinksPerSecond
		nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
		on := !blinking || dt%timePerBlink < timePerBlink/2
		if on {
			carWidth := e.caretWidth(cfg)
			carWidth := e.caretWidth(c)
			carX -= carWidth / 2
			carAsc, carDesc := -lines[carLine].Bounds.Min.Y, lines[carLine].Bounds.Max.Y
			carRect := image.Rectangle{


@@ 249,29 248,29 @@ func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, ctx *layo
			})
			carRect = clip.Intersect(carRect)
			if !carRect.Empty() {
				paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(ops)
				e.Material.Add(ops)
				paint.PaintOp{Rect: toRectF(carRect)}.Add(ops)
				paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(c.Ops)
				e.Material.Add(c.Ops)
				paint.PaintOp{Rect: toRectF(carRect)}.Add(c.Ops)
			}
		}
		if blinking {
			redraw := ui.InvalidateOp{At: nextBlink}
			redraw.Add(ops)
			redraw.Add(c.Ops)
		}
	}
	stack.Pop()

	baseline := e.padTop + e.dims.Baseline
	pointerPadding := cfg.Px(ui.Dp(4))
	pointerPadding := c.Px(ui.Dp(4))
	r := image.Rectangle{Max: e.viewSize}
	r.Min.X -= pointerPadding
	r.Min.Y -= pointerPadding
	r.Max.X += pointerPadding
	r.Max.X += pointerPadding
	pointer.RectAreaOp{Rect: r}.Add(ops)
	e.scroller.Add(ops)
	e.clicker.Add(ops)
	ctx.Dimensions = layout.Dimensions{Size: e.viewSize, Baseline: baseline}
	pointer.RectAreaOp{Rect: r}.Add(c.Ops)
	e.scroller.Add(c.Ops)
	e.clicker.Add(c.Ops)
	c.Dimensions = layout.Dimensions{Size: e.viewSize, Baseline: baseline}
}

// Text returns the contents of the editor.


@@ 550,8 549,8 @@ func (e *Editor) moveEnd() {
	e.carXOff = l.Width + a - x
}

func (e *Editor) scrollToCaret(cfg ui.Config) {
	carWidth := e.caretWidth(cfg)
func (e *Editor) scrollToCaret(c ui.Config) {
	carWidth := e.caretWidth(c)
	carLine, _, x, y := e.layoutCaret()
	l := e.lines[carLine]
	if e.SingleLine {

M ui/text/label.go => ui/text/label.go +9 -9
@@ 92,8 92,8 @@ func (l *lineIterator) Next() (String, f32.Point, bool) {
	return String{}, f32.Point{}, false
}

func (l Label) Layout(ops *ui.Ops, ctx *layout.Context) {
	cs := ctx.Constraints
func (l Label) Layout(c *layout.Context) {
	cs := c.Constraints
	textLayout := l.Face.Layout(l.Text, LayoutOptions{MaxWidth: cs.Width.Max})
	lines := textLayout.Lines
	if max := l.MaxLines; max > 0 && len(lines) > max {


@@ 119,16 119,16 @@ func (l Label) Layout(ops *ui.Ops, ctx *layout.Context) {
		}
		lclip := toRectF(clip).Sub(off)
		var stack ui.StackOp
		stack.Push(ops)
		ui.TransformOp{}.Offset(off).Add(ops)
		l.Face.Path(str).Add(ops)
		stack.Push(c.Ops)
		ui.TransformOp{}.Offset(off).Add(c.Ops)
		l.Face.Path(str).Add(c.Ops)
		// Set a default color in case the material is empty.
		paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(ops)
		l.Material.Add(ops)
		paint.PaintOp{Rect: lclip}.Add(ops)
		paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(c.Ops)
		l.Material.Add(c.Ops)
		paint.PaintOp{Rect: lclip}.Add(c.Ops)
		stack.Pop()
	}
	ctx.Dimensions = dims
	c.Dimensions = dims
}

func toRectF(r image.Rectangle) f32.Rectangle {

M ui/widget/image.go => ui/widget/image.go +5 -5
@@ 25,7 25,7 @@ type Image struct {
	Scale float32
}

func (im Image) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context) {
func (im Image) Layout(c *layout.Context) {
	size := im.Src.Bounds()
	wf, hf := float32(size.Dx()), float32(size.Dy())
	var w, h int


@@ 35,7 35,7 @@ func (im Image) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context) {
	} else {
		w, h = int(wf*im.Scale+.5), int(hf*im.Scale+.5)
	}
	cs := ctx.Constraints
	cs := c.Constraints
	d := image.Point{X: cs.Width.Constrain(w), Y: cs.Height.Constrain(h)}
	aspect := float32(w) / float32(h)
	dw, dh := float32(d.X), float32(d.Y)


@@ 48,7 48,7 @@ func (im Image) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context) {
	dr := f32.Rectangle{
		Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
	}
	paint.ImageOp{Src: im.Src, Rect: im.Rect}.Add(ops)
	paint.PaintOp{Rect: dr}.Add(ops)
	ctx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
	paint.ImageOp{Src: im.Src, Rect: im.Rect}.Add(c.Ops)
	paint.PaintOp{Rect: dr}.Add(c.Ops)
	c.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
}