diff --git a/ui/layout/flex.go b/ui/layout/flex.go index a9e734ba..6a58b47c 100644 --- a/ui/layout/flex.go +++ b/ui/layout/flex.go @@ -94,21 +94,22 @@ func (f *Flex) begin(mode flexMode) { f.macro.Record(f.ops) } -// Rigid begins a child and return its constraints. The main axis is constrained -// to the range from 0 to the remaining space. -func (f *Flex) Rigid() Constraints { +// Rigid lays out a widget with the main axis constrained to the range +// from 0 to the remaining space. +func (f *Flex) Rigid(w Widget) FlexChild { f.begin(modeRigid) mainc := axisMainConstraint(f.Axis, f.cs) mainMax := mainc.Max - f.size if mainMax < 0 { mainMax = 0 } - return axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, f.cs)) + cs := axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, f.cs)) + return f.end(w(cs)) } // 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) Constraints { +func (f *Flex) Flexible(weight float32, w Widget) FlexChild { f.begin(modeFlex) mainc := axisMainConstraint(f.Axis, f.cs) var flexSize int @@ -124,12 +125,13 @@ func (f *Flex) Flexible(weight float32) Constraints { } } submainc := Constraint{Max: flexSize} - return axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, f.cs)) + cs := axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, f.cs)) + return f.end(w(cs)) } // End a child by specifying its dimensions. Pass the returned layout result // to Layout. -func (f *Flex) End(dims Dimensions) FlexChild { +func (f *Flex) end(dims Dimensions) FlexChild { if f.mode <= modeBegun { panic("End called without an active child") } @@ -263,9 +265,9 @@ func axisCrossConstraint(a Axis, cs Constraints) Constraint { func axisConstraints(a Axis, mainc, crossc Constraint) Constraints { if a == Horizontal { - return Constraints{mainc, crossc} + return Constraints{Width: mainc, Height: crossc} } else { - return Constraints{crossc, mainc} + return Constraints{Width: crossc, Height: mainc} } } diff --git a/ui/layout/layout.go b/ui/layout/layout.go index 35a025a9..6e7376fe 100644 --- a/ui/layout/layout.go +++ b/ui/layout/layout.go @@ -39,6 +39,10 @@ type Alignment uint8 // relative to a containing space. type Direction uint8 +// Widget is a function that computes a set of dimensions that +// satisfies the given cosntraints. +type Widget func(cs Constraints) Dimensions + const ( Start Alignment = iota End @@ -90,68 +94,44 @@ func RigidConstraints(size image.Point) Constraints { // Inset adds space around an interface element. type Inset struct { Top, Right, Bottom, Left ui.Value - - stack ui.StackOp - top, right, bottom, left int - begun bool - cs Constraints } // Align aligns an interface element in the available space. type Align struct { Alignment Direction - - macro ui.MacroOp - ops *ui.Ops - begun bool - cs Constraints } -// Begin the inset operation and modify the input constraints to -// account for the insets. -func (in *Inset) Begin(c ui.Config, ops *ui.Ops, cs Constraints) Constraints { - if in.begun { - panic("must End before Begin") - } - in.top = c.Px(in.Top) - in.right = c.Px(in.Right) - in.bottom = c.Px(in.Bottom) - in.left = c.Px(in.Left) - in.begun = true - in.cs = cs +// Layout a widget. +func (in *Inset) Layout(c ui.Config, ops *ui.Ops, cs Constraints, w Widget) Dimensions { + top := c.Px(in.Top) + right := c.Px(in.Right) + bottom := c.Px(in.Bottom) + left := c.Px(in.Left) mcs := cs - mcs.Width.Min -= in.left + in.right - mcs.Width.Max -= in.left + in.right + mcs.Width.Min -= left + right + mcs.Width.Max -= left + right if mcs.Width.Min < 0 { mcs.Width.Min = 0 } if mcs.Width.Max < mcs.Width.Min { mcs.Width.Max = mcs.Width.Min } - mcs.Height.Min -= in.top + in.bottom - mcs.Height.Max -= in.top + in.bottom + mcs.Height.Min -= top + bottom + mcs.Height.Max -= top + bottom if mcs.Height.Min < 0 { mcs.Height.Min = 0 } if mcs.Height.Max < mcs.Height.Min { mcs.Height.Max = mcs.Height.Min } - in.stack.Push(ops) - ui.TransformOp{}.Offset(toPointF(image.Point{X: in.left, Y: in.top})).Add(ops) - return mcs -} - -// End the inset operation and return the dimensions for the -// inset child. -func (in *Inset) End(dims Dimensions) Dimensions { - if !in.begun { - panic("must Begin before End") - } - in.begun = false - in.stack.Pop() + var stack ui.StackOp + stack.Push(ops) + ui.TransformOp{}.Offset(toPointF(image.Point{X: left, Y: top})).Add(ops) + dims := w(mcs) + stack.Pop() return Dimensions{ - Size: in.cs.Constrain(dims.Size.Add(image.Point{X: in.right + in.left, Y: in.top + in.bottom})), - Baseline: dims.Baseline + in.top, + Size: cs.Constrain(dims.Size.Add(image.Point{X: right + left, Y: top + bottom})), + Baseline: dims.Baseline + top, } } @@ -161,35 +141,21 @@ func UniformInset(v ui.Value) Inset { return Inset{Top: v, Right: v, Bottom: v, Left: v} } -// Begin aligning and return the constraints with no minimum size. -func (a *Align) Begin(ops *ui.Ops, cs Constraints) Constraints { - if a.begun { - panic("must End before Begin") - } - a.begun = true - a.ops = ops - a.cs = cs - a.macro.Record(ops) - cs.Width.Min = 0 - cs.Height.Min = 0 - return cs -} - -// End the align operation and return the dimensions for the -// aligned child. -func (a *Align) End(dims Dimensions) Dimensions { - if !a.begun { - panic("must Begin before End") - } - a.begun = false - ops := a.ops - a.macro.Stop() +// Layout a widget. +func (a *Align) Layout(ops *ui.Ops, cs Constraints, w Widget) Dimensions { + var macro ui.MacroOp + mcs := cs + mcs.Width.Min = 0 + mcs.Height.Min = 0 + macro.Record(ops) + dims := w(mcs) + macro.Stop() sz := dims.Size - if sz.X < a.cs.Width.Min { - sz.X = a.cs.Width.Min + if sz.X < cs.Width.Min { + sz.X = cs.Width.Min } - if sz.Y < a.cs.Height.Min { - sz.Y = a.cs.Height.Min + if sz.Y < cs.Height.Min { + sz.Y = cs.Height.Min } var p image.Point switch a.Alignment { @@ -207,7 +173,7 @@ func (a *Align) End(dims Dimensions) Dimensions { var stack ui.StackOp stack.Push(ops) ui.TransformOp{}.Offset(toPointF(p)).Add(ops) - a.macro.Add(ops) + macro.Add(ops) stack.Pop() return Dimensions{ Size: sz, diff --git a/ui/layout/layout_test.go b/ui/layout/layout_test.go index 9efb85bb..f4c958a2 100644 --- a/ui/layout/layout_test.go +++ b/ui/layout/layout_test.go @@ -27,11 +27,12 @@ func ExampleInset() { // Inset all edges by 10. inset := layout.UniformInset(ui.Dp(10)) - cs = inset.Begin(cfg, ops, cs) - // Lay out a 50x50 sized widget. - dims := layoutWidget(50, 50, cs) - fmt.Println(dims.Size) - dims = inset.End(dims) + dims := inset.Layout(cfg, ops, cs, func(cs layout.Constraints) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(50, 50, cs) + fmt.Println(dims.Size) + return dims + }) fmt.Println(dims.Size) @@ -47,13 +48,12 @@ func ExampleAlign() { cs := layout.RigidConstraints(image.Point{X: 100, Y: 100}) align := layout.Align{Alignment: layout.Center} - cs = align.Begin(ops, cs) - - // Lay out a 50x50 sized widget. - dims := layoutWidget(50, 50, cs) - fmt.Println(dims.Size) - - dims = align.End(dims) + dims := align.Layout(ops, cs, func(cs layout.Constraints) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(50, 50, cs) + fmt.Println(dims.Size) + return dims + }) fmt.Println(dims.Size) @@ -71,18 +71,18 @@ func ExampleFlex() { flex.Init(ops, cs) // Rigid 10x10 widget. - cs = flex.Rigid() - fmt.Printf("Rigid: %v\n", cs.Width) - dims := layoutWidget(10, 10, cs) - child1 := flex.End(dims) + child1 := flex.Rigid(func(cs layout.Constraints) layout.Dimensions { + fmt.Printf("Rigid: %v\n", cs.Width) + return layoutWidget(10, 10, cs) + }) // Child with 50% space allowance. - cs = flex.Flexible(0.5) - fmt.Printf("50%%: %v\n", cs.Width) - dims = layoutWidget(10, 10, cs) - child2 := flex.End(dims) + child2 := flex.Flexible(0.5, func(cs layout.Constraints) layout.Dimensions { + fmt.Printf("50%%: %v\n", cs.Width) + return layoutWidget(10, 10, cs) + }) - dims = flex.Layout(child1, child2) + flex.Layout(child1, child2) // Output: // Rigid: {0 100} @@ -98,17 +98,17 @@ func ExampleStack() { stack.Init(ops, cs) // Rigid 50x50 widget. - cs = stack.Rigid() - dims := layoutWidget(50, 50, cs) - child1 := stack.End(dims) + child1 := stack.Rigid(func(cs layout.Constraints) layout.Dimensions { + return layoutWidget(50, 50, cs) + }) // Force widget to the same size as the first. - cs = stack.Expand() - fmt.Printf("Expand: %v\n", cs) - dims = layoutWidget(10, 10, cs) - child2 := stack.End(dims) + child2 := stack.Expand(func(cs layout.Constraints) layout.Dimensions { + fmt.Printf("Expand: %v\n", cs) + return layoutWidget(10, 10, cs) + }) - dims = stack.Layout(child1, child2) + stack.Layout(child1, child2) // Output: // Expand: {{50 50} {50 50}} @@ -123,19 +123,14 @@ func ExampleList() { const listLen = 1e6 var list layout.List - list.Init(cfg, q, ops, cs, listLen) count := 0 - for ; list.More(); list.Next() { - dims := layoutWidget(20, 20, list.Constraints()) - list.End(dims) + list.Layout(cfg, q, ops, cs, listLen, func(cs layout.Constraints, i int) layout.Dimensions { count++ - } + return layoutWidget(20, 20, cs) + }) fmt.Println(count) - dims := list.Layout() - _ = dims - // Output: // 5 } diff --git a/ui/layout/list.go b/ui/layout/list.go index 76168790..1d641b0b 100644 --- a/ui/layout/list.go +++ b/ui/layout/list.go @@ -56,6 +56,10 @@ type List struct { dir iterationDir } +// ListElement is a function that computes the dimensions of +// a list element. +type ListElement func(cs Constraints, index int) Dimensions + type iterationDir uint8 const ( @@ -67,8 +71,8 @@ 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) { - if l.More() { +func (l *List) init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int) { + if l.more() { panic("unfinished child") } l.config = cfg @@ -88,7 +92,15 @@ func (l *List) Init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, l l.first = len } l.macro.Record(ops) - l.Next() + l.next() +} + +// 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 { + for l.init(c, q, ops, cs, len); l.more(); l.next() { + l.end(w(l.constraints(), l.index())) + } + return l.layout() } func (l *List) scrollToEnd() bool { @@ -106,23 +118,23 @@ func (l *List) update() { l.offset += d } -// Next advances to the next child. -func (l *List) Next() { - l.dir = l.next() +// next advances to the next child. +func (l *List) next() { + l.dir = l.nextDir() // The user scroll offset is applied after scrolling to // list end. - if l.scrollToEnd() && !l.More() && l.scrollDelta < 0 { + if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { l.beforeEnd = true l.offset += l.scrollDelta - l.dir = l.next() + l.dir = l.nextDir() } - if l.More() { + if l.more() { l.child.Record(l.ops) } } -// Index is current child's position in the underlying list. -func (l *List) Index() int { +// index is current child's position in the underlying list. +func (l *List) index() int { switch l.dir { case iterateBackward: return l.first - 1 @@ -133,17 +145,17 @@ func (l *List) Index() int { } } -// Constraints is the constraints for the current child. -func (l *List) Constraints() Constraints { +// constraints is the constraints for the current child. +func (l *List) constraints() Constraints { return axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.cs)) } -// More reports whether more children are needed. -func (l *List) More() bool { +// more reports whether more children are needed. +func (l *List) more() bool { return l.dir != iterateNone } -func (l *List) next() iterationDir { +func (l *List) nextDir() iterationDir { vsize := axisMainConstraint(l.Axis, l.cs).Max last := l.first + len(l.children) // Clamp offset. @@ -165,7 +177,7 @@ func (l *List) next() iterationDir { } // End the current child by specifying its dimensions. -func (l *List) End(dims Dimensions) { +func (l *List) end(dims Dimensions) { l.child.Stop() child := scrollChild{dims.Size, l.child} mainSize := axisMain(l.Axis, child.size) @@ -184,8 +196,8 @@ func (l *List) End(dims Dimensions) { } // Layout the List and return its dimensions. -func (l *List) Layout() Dimensions { - if l.More() { +func (l *List) layout() Dimensions { + if l.more() { panic("unfinished child") } mainc := axisMainConstraint(l.Axis, l.cs) diff --git a/ui/layout/stack.go b/ui/layout/stack.go index 264d09e1..6016a2ec 100644 --- a/ui/layout/stack.go +++ b/ui/layout/stack.go @@ -51,25 +51,26 @@ func (s *Stack) begin() { s.macro.Record(s.ops) } -// Rigid begins a child with the same constraints that were +// Rigid lays out a widget with the same constraints that were // passed to Init. -func (s *Stack) Rigid() Constraints { +func (s *Stack) Rigid(w Widget) StackChild { s.begin() - return s.cs + return s.end(w(s.cs)) } -// Expand begins a child with constraints that exactly match +// Expand lays out a widget with constraints that exactly match // the biggest child previously added. -func (s *Stack) Expand() Constraints { +func (s *Stack) Expand(w Widget) StackChild { s.begin() - return Constraints{ + cs := Constraints{ 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)) } // End a child by specifying its dimensions. -func (s *Stack) End(dims Dimensions) StackChild { +func (s *Stack) end(dims Dimensions) StackChild { s.macro.Stop() s.begun = false if w := dims.Size.X; w > s.maxSZ.X {