diff --git a/ui/layout/doc.go b/ui/layout/doc.go index f2510cc7..fd9fc888 100644 --- a/ui/layout/doc.go +++ b/ui/layout/doc.go @@ -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) }) }) diff --git a/ui/layout/flex.go b/ui/layout/flex.go index 6a58b47c..c33dd92a 100644 --- a/ui/layout/flex.go +++ b/ui/layout/flex.go @@ -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 { diff --git a/ui/layout/layout.go b/ui/layout/layout.go index e1d40993..e6bb85fc 100644 --- a/ui/layout/layout.go +++ b/ui/layout/layout.go @@ -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, } diff --git a/ui/layout/layout_test.go b/ui/layout/layout_test.go index f4c958a2..839e5d68 100644 --- a/ui/layout/layout_test.go +++ b/ui/layout/layout_test.go @@ -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, diff --git a/ui/layout/list.go b/ui/layout/list.go index 9e367d9b..6f8f5c0d 100644 --- a/ui/layout/list.go +++ b/ui/layout/list.go @@ -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 { diff --git a/ui/layout/stack.go b/ui/layout/stack.go index 3177c4a8..e4f8accc 100644 --- a/ui/layout/stack.go +++ b/ui/layout/stack.go @@ -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, } diff --git a/ui/text/editor.go b/ui/text/editor.go index 5993700c..40a8d504 100644 --- a/ui/text/editor.go +++ b/ui/text/editor.go @@ -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. diff --git a/ui/text/label.go b/ui/text/label.go index d7d69d19..79a94bd0 100644 --- a/ui/text/label.go +++ b/ui/text/label.go @@ -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 { diff --git a/ui/widget/image.go b/ui/widget/image.go index 90dd1599..0a33c6ee 100644 --- a/ui/widget/image.go +++ b/ui/widget/image.go @@ -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} }