Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Commit

Permalink
layout: change Widget to take explicit Context and return explicit Di…
Browse files Browse the repository at this point in the history
…mensions

Change the definition of Widget from the implicit

        type Widget func()

to the explicit functional

        type Widget func(gtx layout.Context) layout.Dimensions

The advantages are numerous:

- Clearer connection between the incoming context and the output dimensions.
- Returning the Dimensions are impossible to omit.
- Contexts passed by value, so its fields can be exported
and freely mutated by the program.

The only disadvantage is the longer function literals and the many "returns".
What tipped the scales in favour of the explicit Widget variant is that type
aliases can dramatically shorten the literals:

	type (
		C = layout.Context
		D = layout.Dimensions
	)

	widget := func(gtx C) D {
		...
	}

Note that the aliases are not part of the Gio API and it is up to each user
whether they want to use them.

Finally the Go proposal for lightweight function literals,
golang/go#21498, may remove the disadvantage
completely in future.

Context becomes a plain struct with only public fields, and its Reset is
replaced by a NewContext convenience constructor.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
  • Loading branch information
eliasnaur committed May 23, 2020
1 parent af10307 commit 3af01a3
Show file tree
Hide file tree
Showing 25 changed files with 226 additions and 208 deletions.
65 changes: 28 additions & 37 deletions layout/context.go
Expand Up @@ -20,63 +20,54 @@ type Context struct {
// Constraints track the constraints for the active widget or
// layout.
Constraints Constraints
// Dimensions track the result of the most recent layout
// operation.
Dimensions Dimensions

cfg system.Config
queue event.Queue
Config system.Config
Queue event.Queue
*op.Ops
}

// layout a widget with a set of constraints and return its
// dimensions. The widget dimensions are constrained and the previous
// constraints are restored after layout.
func ctxLayout(gtx *Context, cs Constraints, w Widget) Dimensions {
saved := gtx.Constraints
gtx.Constraints = cs
gtx.Dimensions = Dimensions{}
w()
gtx.Dimensions.Size = cs.Constrain(gtx.Dimensions.Size)
gtx.Constraints = saved
return gtx.Dimensions
}

// Reset the context. The constraints' minimum and maximum values are
// set to the size.
func (c *Context) Reset(q event.Queue, cfg system.Config, size image.Point) {
c.Constraints = Constraints{Min: size, Max: size}
c.Dimensions = Dimensions{}
c.cfg = cfg
c.queue = q
if c.Ops == nil {
c.Ops = new(op.Ops)
// NewContext is a shorthand for
//
// Context{
// Ops: ops,
// Queue: q,
// Config: cfg,
// Constraints: Exact(size),
// }
//
// NewContext calls ops.Reset.
func NewContext(ops *op.Ops, q event.Queue, cfg system.Config, size image.Point) Context {
ops.Reset()
return Context{
Ops: ops,
Queue: q,
Config: cfg,
Constraints: Exact(size),
}
c.Ops.Reset()
}

// Now returns the configuration time or the zero time.
func (c *Context) Now() time.Time {
if c.cfg == nil {
func (c Context) Now() time.Time {
if c.Config == nil {
return time.Time{}
}
return c.cfg.Now()
return c.Config.Now()
}

// Px maps the value to pixels. If no configuration is set,
// Px returns the rounded value of v.
func (c *Context) Px(v unit.Value) int {
if c.cfg == nil {
func (c Context) Px(v unit.Value) int {
if c.Config == nil {
return int(math.Round(float64(v.V)))
}
return c.cfg.Px(v)
return c.Config.Px(v)
}

// Events returns the events available for the key. If no
// queue is configured, Events returns nil.
func (c *Context) Events(k event.Tag) []event.Event {
if c.queue == nil {
func (c Context) Events(k event.Tag) []event.Event {
if c.Queue == nil {
return nil
}
return c.queue.Events(k)
return c.Queue.Events(k)
}
15 changes: 7 additions & 8 deletions layout/doc.go
Expand Up @@ -13,17 +13,16 @@ in an implicit Context to keep the Widget declaration short.
For example, to add space above a widget:
gtx := new(layout.Context)
gtx.Reset(...)
var gtx layout.Context
// Configure a top inset.
inset := layout.Inset{Top: unit.Dp(8), ...}
// Use the inset to lay out a widget.
inset.Layout(gtx, func() {
// Lay out widget and determine its size given the constraints.
// Lay out widget and determine its size given the constraints
// in gtx.Constraints.
...
dims := layout.Dimensions{...}
gtx.Dimensions = dims
return layout.Dimensions{...}
})
Note that the example does not generate any garbage even though the
Expand All @@ -37,10 +36,10 @@ be created from a few generic layouts.
This example both aligns and insets a child:
inset := layout.Inset{...}
inset.Layout(gtx, func() {
inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
align := layout.Align(...)
align.Layout(gtx, func() {
widget.Layout(gtx, ...)
return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return widget.Layout(gtx, ...)
})
})
Expand Down
85 changes: 51 additions & 34 deletions layout/example_test.go
Expand Up @@ -5,62 +5,73 @@ import (
"image"

"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)

func ExampleInset() {
gtx := new(layout.Context)
gtx.Reset(nil, nil, image.Point{X: 100, Y: 100})
// Loose constraints with no minimal size.
gtx.Constraints.Min = image.Point{}
gtx := layout.Context{
Ops: new(op.Ops),
// Loose constraints with no minimal size.
Constraints: layout.Constraints{
Max: image.Point{X: 100, Y: 100},
},
}

// Inset all edges by 10.
inset := layout.UniformInset(unit.Dp(10))
inset.Layout(gtx, func() {
dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
// Lay out a 50x50 sized widget.
layoutWidget(gtx, 50, 50)
fmt.Println(gtx.Dimensions.Size)
dims := layoutWidget(gtx, 50, 50)
fmt.Println(dims.Size)
return dims
})

fmt.Println(gtx.Dimensions.Size)
fmt.Println(dims.Size)

// Output:
// (50,50)
// (70,70)
}

func ExampleDirection() {
gtx := new(layout.Context)
// Rigid constraints with both minimum and maximum set.
gtx.Reset(nil, nil, image.Point{X: 100, Y: 100})
gtx := layout.Context{
Ops: new(op.Ops),
// Rigid constraints with both minimum and maximum set.
Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
}

layout.Center.Layout(gtx, func() {
dims := layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
// Lay out a 50x50 sized widget.
layoutWidget(gtx, 50, 50)
fmt.Println(gtx.Dimensions.Size)
dims := layoutWidget(gtx, 50, 50)
fmt.Println(dims.Size)
return dims
})

fmt.Println(gtx.Dimensions.Size)
fmt.Println(dims.Size)

// Output:
// (50,50)
// (100,100)
}

func ExampleFlex() {
gtx := new(layout.Context)
gtx.Reset(nil, nil, image.Point{X: 100, Y: 100})
gtx := layout.Context{
Ops: new(op.Ops),
// Rigid constraints with both minimum and maximum set.
Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
}

layout.Flex{}.Layout(gtx,
// Rigid 10x10 widget.
layout.Rigid(func() {
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
fmt.Printf("Rigid: %v\n", gtx.Constraints)
layoutWidget(gtx, 10, 10)
return layoutWidget(gtx, 10, 10)
}),
// Child with 50% space allowance.
layout.Flexed(0.5, func() {
layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions {
fmt.Printf("50%%: %v\n", gtx.Constraints)
layoutWidget(gtx, 10, 10)
return layoutWidget(gtx, 10, 10)
}),
)

Expand All @@ -70,19 +81,22 @@ func ExampleFlex() {
}

func ExampleStack() {
gtx := new(layout.Context)
gtx.Reset(nil, nil, image.Point{X: 100, Y: 100})
gtx.Constraints.Min = image.Point{}
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Point{X: 100, Y: 100},
},
}

layout.Stack{}.Layout(gtx,
// Force widget to the same size as the second.
layout.Expanded(func() {
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
fmt.Printf("Expand: %v\n", gtx.Constraints)
layoutWidget(gtx, 10, 10)
return layoutWidget(gtx, 10, 10)
}),
// Rigid 50x50 widget.
layout.Stacked(func() {
layoutWidget(gtx, 50, 50)
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layoutWidget(gtx, 50, 50)
}),
)

Expand All @@ -91,17 +105,20 @@ func ExampleStack() {
}

func ExampleList() {
gtx := new(layout.Context)
gtx.Reset(nil, nil, image.Point{X: 100, Y: 100})
gtx := layout.Context{
Ops: new(op.Ops),
// Rigid constraints with both minimum and maximum set.
Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
}

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

var list layout.List
count := 0
list.Layout(gtx, listLen, func(i int) {
list.Layout(gtx, listLen, func(gtx layout.Context, i int) layout.Dimensions {
count++
layoutWidget(gtx, 20, 20)
return layoutWidget(gtx, 20, 20)
})

fmt.Println(count)
Expand All @@ -110,8 +127,8 @@ func ExampleList() {
// 5
}

func layoutWidget(ctx *layout.Context, width, height int) {
ctx.Dimensions = layout.Dimensions{
func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions {
return layout.Dimensions{
Size: image.Point{
X: width,
Y: height,
Expand Down
12 changes: 8 additions & 4 deletions layout/flex.go
Expand Up @@ -74,7 +74,7 @@ func Flexed(weight float32, widget Widget) FlexChild {
// Layout a list of children. The position of the children are
// determined by the specified order, but Rigid children are laid out
// before Flexed children.
func (f Flex) Layout(gtx *Context, children ...FlexChild) {
func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
size := 0
// Lay out Rigid children.
for i, child := range children {
Expand All @@ -91,7 +91,9 @@ func (f Flex) Layout(gtx *Context, children ...FlexChild) {
cs = axisConstraints(f.Axis, 0, mainMax, crossMin, crossMax)
var m op.MacroOp
m.Record(gtx.Ops)
dims := ctxLayout(gtx, cs, child.widget)
gtx := gtx
gtx.Constraints = cs
dims := child.widget(gtx)
m.Stop()
sz := axisMain(f.Axis, dims.Size)
size += sz
Expand Down Expand Up @@ -124,7 +126,9 @@ func (f Flex) Layout(gtx *Context, children ...FlexChild) {
cs = axisConstraints(f.Axis, flexSize, flexSize, crossMin, crossMax)
var m op.MacroOp
m.Record(gtx.Ops)
dims := ctxLayout(gtx, cs, child.widget)
gtx := gtx
gtx.Constraints = cs
dims := child.widget(gtx)
m.Stop()
sz := axisMain(f.Axis, dims.Size)
size += sz
Expand Down Expand Up @@ -200,7 +204,7 @@ func (f Flex) Layout(gtx *Context, children ...FlexChild) {
mainSize += space / (len(children) * 2)
}
sz := axisPoint(f.Axis, mainSize, maxCross)
gtx.Dimensions = Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
}

func axisPoint(a Axis, main, cross int) image.Point {
Expand Down
18 changes: 9 additions & 9 deletions layout/layout.go
Expand Up @@ -40,7 +40,7 @@ type Direction uint8

// Widget is a function scope for drawing, processing events and
// computing dimensions for a user interface element.
type Widget func()
type Widget func(gtx Context) Dimensions

const (
Start Alignment = iota
Expand Down Expand Up @@ -111,7 +111,7 @@ type Inset struct {
}

// Layout a widget.
func (in Inset) Layout(gtx *Context, w Widget) {
func (in Inset) Layout(gtx Context, w Widget) Dimensions {
top := gtx.Px(in.Top)
right := gtx.Px(in.Right)
bottom := gtx.Px(in.Bottom)
Expand All @@ -138,9 +138,10 @@ func (in Inset) Layout(gtx *Context, w Widget) {
var stack op.StackOp
stack.Push(gtx.Ops)
op.TransformOp{}.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops)
dims := ctxLayout(gtx, mcs, w)
gtx.Constraints = mcs
dims := w(gtx)
stack.Pop()
gtx.Dimensions = Dimensions{
return Dimensions{
Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
Baseline: dims.Baseline + bottom,
}
Expand All @@ -153,13 +154,12 @@ func UniformInset(v unit.Value) Inset {
}

// Layout a widget according to the direction.
func (a Direction) Layout(gtx *Context, w Widget) {
func (a Direction) Layout(gtx Context, w Widget) Dimensions {
var macro op.MacroOp
macro.Record(gtx.Ops)
cs := gtx.Constraints
mcs := cs
mcs.Min = image.Point{}
dims := ctxLayout(gtx, mcs, w)
gtx.Constraints.Min = image.Point{}
dims := w(gtx)
macro.Stop()
sz := dims.Size
if sz.X < cs.Min.X {
Expand All @@ -186,7 +186,7 @@ func (a Direction) Layout(gtx *Context, w Widget) {
op.TransformOp{}.Offset(FPt(p)).Add(gtx.Ops)
macro.Add()
stack.Pop()
gtx.Dimensions = Dimensions{
return Dimensions{
Size: sz,
Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
}
Expand Down

0 comments on commit 3af01a3

Please sign in to comment.