ui/layout: introduce Context

Context keeps the current Constraints and Dimensions so the layout
function scopes don't have to.

With

	ctx := new(layout.Context)

a label with margins and alignment goes from

	return al.Layout(ops, cs, func(cs layout.Constraints) layout.Dimensions {
		in := layout.Inset{...}
		return in.Layout(c, ops, cs, func(cs layout.Constraints) layout.Dimensions {
			return text.Label{...}.Layout(ops, cs)
		})
	})

to

	al.Layout(ops, ctx, func() {
		in := layout.Inset{...}
		in.Layout(c, ops, ctx, func() {
		       text.Label{...}.Layout(ops, ctx)
		})
	})

It was a difficult trade-off between the verbose functional approach
and the shorter but more complex Context.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-09-24 19:06:07 +02:00
parent 64add13d28
commit ce9bcee62b
9 changed files with 126 additions and 89 deletions
+15 -13
View File
@@ -5,28 +5,30 @@ Package layout implements layouts common to GUI programs.
Constraints and dimensions
Constraints and dimensions form the interface between
layouts and interface child elements. Every layout operation
start with a set of constraints for acceptable widths and heights
of a child. The operation ends with the child computing and returning
its size and baseline (if any).
Constraints and dimensions form the interface between layouts and
interface child elements. This package operates on Widgets, functions
that compute Dimensions from a a set of constraints for acceptable
widths and heights. Both the constraints and dimensions are maintained
in an implicit Context to keep the Widget declaration short.
For example, to add space above a widget:
var cs layout.Constraints = ...
ctx := new(layout.Context)
ctx.Constraints = ...
// Configure a top inset.
inset := layout.Inset{Top: ui.Dp(8), ...}
// Use the inset to lay out a widget.
inset.Layout(..., cs, func(cs layout.Constraints) layout.Dimensions {
inset.Layout(..., ctx, func() {
// Lay out widget and determine its size given the constraints.
dimensions := widget.Layout(..., cs)
return dimensions
...
dims := layout.Dimensions{...}
ctx.Dimensions = dims
})
Note that the example does not generate any garbage even though the
Inset is transient. Layouts that don't accept user input are designed
to escape to the heap during their use.
to not escape to the heap during their use.
Layout operations are recursive: a child in a layout operation can
itself be another layout. That way, complex user interfaces can
@@ -35,10 +37,10 @@ be created from a few generic layouts.
This example both aligns and insets a child:
inset := layout.Inset{...}
inset.Layout(..., cs, func(cs layout.Constraints) layout.Dimensions {
inset.Layout(..., ctx, func() {
align := layout.Align{...}
return align.Layout(..., cs, func(cs layout.Constraints) layout.Dimensions {
return widget.Layout(..., cs)
align.Layout(..., ctx, func() {
widget.Layout(..., ctx)
})
})
+18 -13
View File
@@ -20,9 +20,9 @@ type Flex struct {
// Alignment is the alignment in the cross axis.
Alignment Alignment
ctx *Context
macro ui.MacroOp
ops *ui.Ops
cs Constraints
mode flexMode
size int
rigidSize int
@@ -69,13 +69,13 @@ const (
)
// Init must be called before Rigid or Flexible.
func (f *Flex) Init(ops *ui.Ops, cs Constraints) *Flex {
func (f *Flex) Init(ops *ui.Ops, ctx *Context) *Flex {
if f.mode > modeBegun {
panic("must End the current child before calling Init again")
}
f.mode = modeBegun
f.ops = ops
f.cs = cs
f.ctx = ctx
f.size = 0
f.rigidSize = 0
f.maxCross = 0
@@ -98,20 +98,23 @@ func (f *Flex) begin(mode flexMode) {
// from 0 to the remaining space.
func (f *Flex) Rigid(w Widget) FlexChild {
f.begin(modeRigid)
mainc := axisMainConstraint(f.Axis, f.cs)
cs := f.ctx.Constraints
mainc := axisMainConstraint(f.Axis, cs)
mainMax := mainc.Max - f.size
if mainMax < 0 {
mainMax = 0
}
cs := axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, f.cs))
return f.end(w(cs))
cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs))
dims := f.ctx.Layout(cs, w)
return f.end(dims)
}
// Flexible is like Rigid, where the main axis size is also constrained to a
// fraction of the space not taken up by Rigid children.
func (f *Flex) Flexible(weight float32, w Widget) FlexChild {
f.begin(modeFlex)
mainc := axisMainConstraint(f.Axis, f.cs)
cs := f.ctx.Constraints
mainc := axisMainConstraint(f.Axis, cs)
var flexSize int
if mainc.Max > f.size {
flexSize = mainc.Max - f.rigidSize
@@ -125,8 +128,9 @@ func (f *Flex) Flexible(weight float32, w Widget) FlexChild {
}
}
submainc := Constraint{Max: flexSize}
cs := axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, f.cs))
return f.end(w(cs))
cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs))
dims := f.ctx.Layout(cs, w)
return f.end(dims)
}
// End a child by specifying its dimensions. Pass the returned layout result
@@ -153,9 +157,10 @@ func (f *Flex) end(dims Dimensions) FlexChild {
// Layout a list of children. The order of the children determines their laid
// out order.
func (f *Flex) Layout(children ...FlexChild) Dimensions {
mainc := axisMainConstraint(f.Axis, f.cs)
crossSize := axisCrossConstraint(f.Axis, f.cs).Constrain(f.maxCross)
func (f *Flex) Layout(children ...FlexChild) {
cs := f.ctx.Constraints
mainc := axisMainConstraint(f.Axis, cs)
crossSize := axisCrossConstraint(f.Axis, cs).Constrain(f.maxCross)
var space int
if mainc.Min > f.size {
space = mainc.Min - f.size
@@ -220,7 +225,7 @@ func (f *Flex) Layout(children ...FlexChild) Dimensions {
if baseline == 0 {
baseline = sz.Y
}
return Dimensions{Size: sz, Baseline: baseline}
f.ctx.Dimensions = Dimensions{Size: sz, Baseline: baseline}
}
func axisPoint(a Axis, main, cross int) image.Point {
+31 -12
View File
@@ -37,9 +37,16 @@ type Alignment uint8
// space.
type Direction uint8
// Widget is a function that computes a set of dimensions that
// satisfies the given constraints.
type Widget func(cs Constraints) Dimensions
// Widget is a function scope for drawing, processing input and
// computing dimensions for a user interface element.
type Widget func()
// Context tracks the current constraints and dimensions during
// layout.
type Context struct {
Constraints Constraints
Dimensions Dimensions
}
const (
Start Alignment = iota
@@ -65,6 +72,17 @@ const (
Vertical
)
// Layout a widget with a set of constraints and return its
// dimensions. The previous constraints are restored after layout.
func (s *Context) Layout(cs Constraints, w Widget) Dimensions {
saved := s.Constraints
s.Constraints = cs
s.Dimensions = Dimensions{}
w()
s.Constraints = saved
return s.Dimensions
}
// Constrain a value to the range [Min; Max].
func (c Constraint) Constrain(v int) int {
if v < c.Min {
@@ -100,12 +118,12 @@ type Align struct {
}
// Layout a widget.
func (in *Inset) Layout(c ui.Config, ops *ui.Ops, cs Constraints, w Widget) Dimensions {
func (in Inset) Layout(c ui.Config, ops *ui.Ops, ctx *Context, w Widget) {
top := c.Px(in.Top)
right := c.Px(in.Right)
bottom := c.Px(in.Bottom)
left := c.Px(in.Left)
mcs := cs
mcs := ctx.Constraints
mcs.Width.Min -= left + right
mcs.Width.Max -= left + right
if mcs.Width.Min < 0 {
@@ -125,10 +143,10 @@ func (in *Inset) Layout(c ui.Config, ops *ui.Ops, cs Constraints, w Widget) Dime
var stack ui.StackOp
stack.Push(ops)
ui.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(ops)
dims := w(mcs)
dims := ctx.Layout(mcs, w)
stack.Pop()
return Dimensions{
Size: cs.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})),
ctx.Dimensions = Dimensions{
Size: ctx.Constraints.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})),
Baseline: dims.Baseline + top,
}
}
@@ -140,13 +158,14 @@ func UniformInset(v ui.Value) Inset {
}
// Layout a widget.
func (a *Align) Layout(ops *ui.Ops, cs Constraints, w Widget) Dimensions {
func (a Align) Layout(ops *ui.Ops, st *Context, w Widget) {
var macro ui.MacroOp
macro.Record(ops)
cs := st.Constraints
mcs := cs
mcs.Width.Min = 0
mcs.Height.Min = 0
macro.Record(ops)
dims := w(mcs)
dims := st.Layout(mcs, w)
macro.Stop()
sz := dims.Size
if sz.X < cs.Width.Min {
@@ -173,7 +192,7 @@ func (a *Align) Layout(ops *ui.Ops, cs Constraints, w Widget) Dimensions {
ui.TransformOp{}.Offset(toPointF(p)).Add(ops)
macro.Add(ops)
stack.Pop()
return Dimensions{
st.Dimensions = Dimensions{
Size: sz,
Baseline: dims.Baseline,
}
+36 -34
View File
@@ -19,22 +19,21 @@ var cfg = new(config)
func ExampleInset() {
ops := new(ui.Ops)
ctx := new(layout.Context)
// Loose constraints with no minimal size.
var cs layout.Constraints
cs.Width.Max = 100
cs.Height.Max = 100
ctx.Constraints.Width.Max = 100
ctx.Constraints.Height.Max = 100
// Inset all edges by 10.
inset := layout.UniformInset(ui.Dp(10))
dims := inset.Layout(cfg, ops, cs, func(cs layout.Constraints) layout.Dimensions {
inset.Layout(cfg, ops, ctx, func() {
// Lay out a 50x50 sized widget.
dims := layoutWidget(50, 50, cs)
fmt.Println(dims.Size)
return dims
layoutWidget(ctx, 50, 50)
fmt.Println(ctx.Dimensions.Size)
})
fmt.Println(dims.Size)
fmt.Println(ctx.Dimensions.Size)
// Output:
// (50,50)
@@ -43,19 +42,19 @@ func ExampleInset() {
func ExampleAlign() {
ops := new(ui.Ops)
ctx := new(layout.Context)
// Rigid constraints with both minimum and maximum set.
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
align := layout.Align{Alignment: layout.Center}
dims := align.Layout(ops, cs, func(cs layout.Constraints) layout.Dimensions {
align.Layout(ops, ctx, func() {
// Lay out a 50x50 sized widget.
dims := layoutWidget(50, 50, cs)
fmt.Println(dims.Size)
return dims
layoutWidget(ctx, 50, 50)
fmt.Println(ctx.Dimensions.Size)
})
fmt.Println(dims.Size)
fmt.Println(ctx.Dimensions.Size)
// Output:
// (50,50)
@@ -64,22 +63,23 @@ func ExampleAlign() {
func ExampleFlex() {
ops := new(ui.Ops)
ctx := new(layout.Context)
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
flex := layout.Flex{}
flex.Init(ops, cs)
flex.Init(ops, ctx)
// Rigid 10x10 widget.
child1 := flex.Rigid(func(cs layout.Constraints) layout.Dimensions {
fmt.Printf("Rigid: %v\n", cs.Width)
return layoutWidget(10, 10, cs)
child1 := flex.Rigid(func() {
fmt.Printf("Rigid: %v\n", ctx.Constraints.Width)
layoutWidget(ctx, 10, 10)
})
// Child with 50% space allowance.
child2 := flex.Flexible(0.5, func(cs layout.Constraints) layout.Dimensions {
fmt.Printf("50%%: %v\n", cs.Width)
return layoutWidget(10, 10, cs)
child2 := flex.Flexible(0.5, func() {
fmt.Printf("50%%: %v\n", ctx.Constraints.Width)
layoutWidget(ctx, 10, 10)
})
flex.Layout(child1, child2)
@@ -91,21 +91,22 @@ func ExampleFlex() {
func ExampleStack() {
ops := new(ui.Ops)
ctx := new(layout.Context)
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
ctx.Constraints = layout.RigidConstraints(image.Point{X: 100, Y: 100})
stack := layout.Stack{}
stack.Init(ops, cs)
stack.Init(ops, ctx)
// Rigid 50x50 widget.
child1 := stack.Rigid(func(cs layout.Constraints) layout.Dimensions {
return layoutWidget(50, 50, cs)
child1 := stack.Rigid(func() {
layoutWidget(ctx, 50, 50)
})
// Force widget to the same size as the first.
child2 := stack.Expand(func(cs layout.Constraints) layout.Dimensions {
fmt.Printf("Expand: %v\n", cs)
return layoutWidget(10, 10, cs)
child2 := stack.Expand(func() {
fmt.Printf("Expand: %v\n", ctx.Constraints)
layoutWidget(ctx, 10, 10)
})
stack.Layout(child1, child2)
@@ -116,17 +117,18 @@ func ExampleStack() {
func ExampleList() {
ops := new(ui.Ops)
ctx := new(layout.Context)
cs := layout.RigidConstraints(image.Point{X: 100, Y: 100})
ctx.Constraints = layout.RigidConstraints(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(cfg, q, ops, cs, listLen, func(cs layout.Constraints, i int) layout.Dimensions {
list.Layout(cfg, q, ops, ctx, listLen, func(i int) {
count++
return layoutWidget(20, 20, cs)
layoutWidget(ctx, 20, 20)
})
fmt.Println(count)
@@ -135,8 +137,8 @@ func ExampleList() {
// 5
}
func layoutWidget(width, height int, cs layout.Constraints) layout.Dimensions {
return layout.Dimensions{
func layoutWidget(ctx *layout.Context, width, height int) {
ctx.Dimensions = layout.Dimensions{
Size: image.Point{
X: width,
Y: height,
+8 -4
View File
@@ -58,7 +58,7 @@ type List struct {
// ListElement is a function that computes the dimensions of
// a list element.
type ListElement func(cs Constraints, index int) Dimensions
type ListElement func(index int)
type iterationDir uint8
@@ -96,12 +96,16 @@ func (l *List) init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, l
}
// Layout the List and return its dimensions.
func (l *List) Layout(c ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int, w ListElement) 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))
l.end(w(cs, l.index()))
i := l.index()
l.end(ctx.Layout(cs, func() {
w(i)
}))
}
return l.layout()
ctx.Dimensions = l.layout()
}
func (l *List) scrollToEnd() bool {
+9 -7
View File
@@ -18,7 +18,7 @@ type Stack struct {
macro ui.MacroOp
ops *ui.Ops
constrained bool
cs Constraints
ctx *Context
maxSZ image.Point
baseline int
}
@@ -30,9 +30,9 @@ type StackChild struct {
}
// Init a stack before calling Rigid or Expand.
func (s *Stack) Init(ops *ui.Ops, cs Constraints) *Stack {
func (s *Stack) Init(ops *ui.Ops, ctx *Context) *Stack {
s.ops = ops
s.cs = cs
s.ctx = ctx
s.constrained = true
s.maxSZ = image.Point{}
s.baseline = 0
@@ -50,7 +50,8 @@ func (s *Stack) begin() {
// passed to Init.
func (s *Stack) Rigid(w Widget) StackChild {
s.begin()
return s.end(w(s.cs))
dims := s.ctx.Layout(s.ctx.Constraints, w)
return s.end(dims)
}
// Expand lays out a widget with constraints that exactly match
@@ -61,7 +62,8 @@ func (s *Stack) Expand(w Widget) StackChild {
Width: Constraint{Min: s.maxSZ.X, Max: s.maxSZ.X},
Height: Constraint{Min: s.maxSZ.Y, Max: s.maxSZ.Y},
}
return s.end(w(cs))
dims := s.ctx.Layout(cs, w)
return s.end(dims)
}
// End a child by specifying its dimensions.
@@ -83,7 +85,7 @@ func (s *Stack) end(dims Dimensions) StackChild {
// Layout a list of children. The order of the children determines their laid
// out order.
func (s *Stack) Layout(children ...StackChild) Dimensions {
func (s *Stack) Layout(children ...StackChild) {
for _, ch := range children {
sz := ch.dims.Size
var p image.Point
@@ -109,7 +111,7 @@ func (s *Stack) Layout(children ...StackChild) Dimensions {
if b == 0 {
b = s.maxSZ.Y
}
return Dimensions{
s.ctx.Dimensions = Dimensions{
Size: s.maxSZ,
Baseline: b,
}
+3 -2
View File
@@ -165,7 +165,8 @@ func (e *Editor) Focus() {
e.requestFocus = true
}
func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, cs layout.Constraints) layout.Dimensions {
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) {
}
twoDp := cfg.Px(ui.Dp(2))
@@ -270,7 +271,7 @@ func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, cs layout
pointer.RectAreaOp{Rect: r}.Add(ops)
e.scroller.Add(ops)
e.clicker.Add(ops)
return layout.Dimensions{Size: e.viewSize, Baseline: baseline}
ctx.Dimensions = layout.Dimensions{Size: e.viewSize, Baseline: baseline}
}
// Text returns the contents of the editor.
+3 -2
View File
@@ -92,7 +92,8 @@ func (l *lineIterator) Next() (String, f32.Point, bool) {
return String{}, f32.Point{}, false
}
func (l Label) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimensions {
func (l Label) Layout(ops *ui.Ops, ctx *layout.Context) {
cs := ctx.Constraints
textLayout := l.Face.Layout(l.Text, LayoutOptions{MaxWidth: cs.Width.Max})
lines := textLayout.Lines
if max := l.MaxLines; max > 0 && len(lines) > max {
@@ -127,7 +128,7 @@ func (l Label) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimensions {
paint.PaintOp{Rect: lclip}.Add(ops)
stack.Pop()
}
return dims
ctx.Dimensions = dims
}
func toRectF(r image.Rectangle) f32.Rectangle {
+3 -2
View File
@@ -25,7 +25,7 @@ type Image struct {
Scale float32
}
func (im Image) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimensions {
func (im Image) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context) {
size := im.Src.Bounds()
wf, hf := float32(size.Dx()), float32(size.Dy())
var w, h int
@@ -35,6 +35,7 @@ func (im Image) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.D
} else {
w, h = int(wf*im.Scale+.5), int(hf*im.Scale+.5)
}
cs := ctx.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)
@@ -49,5 +50,5 @@ func (im Image) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.D
}
paint.ImageOp{Src: im.Src, Rect: im.Rect}.Add(ops)
paint.PaintOp{Rect: dr}.Add(ops)
return layout.Dimensions{Size: d, Baseline: d.Y}
ctx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
}