diff --git a/ui/button.go b/ui/button.go index 819d352..6345c23 100644 --- a/ui/button.go +++ b/ui/button.go @@ -10,9 +10,11 @@ type Button struct { Text string } -func BuildButton(text string, factory func(b *Button)) *Button { +func BuildButton(text string, fn func(b *Button)) *Button { var b = &Button{Text: text} - factory(b) + if fn != nil { + fn(b) + } return b } diff --git a/ui/fixed.go b/ui/fixed.go index 165aeb0..9d2287a 100644 --- a/ui/fixed.go +++ b/ui/fixed.go @@ -1,42 +1,23 @@ package ui -import ( - "opslag.de/schobers/geom" -) - -var _ Control = &stretch{} - -type fixed struct { - Proxy - - size geom.PointF32 -} - -func (f *fixed) DesiredSize(ctx Context) geom.PointF32 { - w, h := f.size.X, f.size.Y - if geom.IsNaN32(w) || geom.IsNaN32(h) { - child := f.Content.DesiredSize(ctx) - if geom.IsNaN32(w) { - w = child.X - } - if geom.IsNaN32(h) { - h = child.Y - } - } - return geom.PtF32(w, h) -} - -// Fixed wraps the supplied control to fill exactly the space specified. -func Fixed(c Control, w, h float32) Control { - return &fixed{Proxy{Content: c}, geom.PtF32(w, h)} +// FixedSize wraps the supplied control to fill exactly the space specified. +func FixedSize(c Control, w, h float32) Control { + return BuildSpacing(c, func(s *Spacing) { + s.Width = Fixed(w) + s.Height = Fixed(h) + }) } // FixedHeight wraps the supplied control to fill exactly the height specified. Width is taken from wrapped control. func FixedHeight(c Control, h float32) Control { - return Fixed(c, geom.NaN32(), h) + return BuildSpacing(c, func(s *Spacing) { + s.Height = Fixed(h) + }) } // FixedWidth wraps the supplied control to fill exactly the width specified. Height is taken from wrapped control. func FixedWidth(c Control, w float32) Control { - return Fixed(c, w, geom.NaN32()) + return BuildSpacing(c, func(s *Spacing) { + s.Width = Fixed(w) + }) } diff --git a/ui/label.go b/ui/label.go index eba42fe..8d2cdf7 100644 --- a/ui/label.go +++ b/ui/label.go @@ -10,9 +10,11 @@ type Label struct { Text string } -func BuildLabel(text string, factory func(*Label)) *Label { +func BuildLabel(text string, fn func(*Label)) *Label { var l = &Label{Text: text} - factory(l) + if fn != nil { + fn(l) + } return l } diff --git a/ui/length.go b/ui/length.go new file mode 100644 index 0000000..d6f23b5 --- /dev/null +++ b/ui/length.go @@ -0,0 +1,32 @@ +package ui + +import "opslag.de/schobers/geom" + +type Length struct { + float32 +} + +func (l *Length) Value() float32 { + if l == nil { + return 0 + } + return l.float32 +} + +func (l *Length) Zero(value float32) *Length { + if l == nil { + return &Length{value} + } + return l +} + +func Fixed(l float32) *Length { return &Length{l} } + +func Infinite() *Length { return &Length{geom.NaN32()} } + +type SideLengths struct { + Left *Length + Top *Length + Right *Length + Bottom *Length +} diff --git a/ui/scrollbar.go b/ui/scrollbar.go index c7ca03b..b7cef16 100644 --- a/ui/scrollbar.go +++ b/ui/scrollbar.go @@ -28,7 +28,9 @@ func BuildScrollbar(o Orientation, fn func(s *Scrollbar)) *Scrollbar { var offset = (s.Orientation.LengthParallel(move) - s.Orientation.LengthParallel(start)) / handleMaxOffset s.Offset = geom.Max32(0, geom.Min32(s.startDragOffset+offset*hidden, hidden)) }) - fn(s) + if fn != nil { + fn(s) + } return s } diff --git a/ui/spacing.go b/ui/spacing.go new file mode 100644 index 0000000..00631ed --- /dev/null +++ b/ui/spacing.go @@ -0,0 +1,95 @@ +package ui + +import ( + "opslag.de/schobers/geom" +) + +type Spacing struct { + Proxy + + Margin SideLengths + Width *Length + Height *Length +} + +func BuildSpacing(content Control, fn func(*Spacing)) *Spacing { + s := &Spacing{} + s.Content = content + if fn != nil { + fn(s) + } + return s +} + +func sumLengths(lengths ...float32) (float32, int) { + var fixed float32 + var infinite int + for _, l := range lengths { + if geom.IsNaN32(l) { + infinite++ + } else { + fixed += l + } + } + return fixed, infinite +} + +func addMargin(bounds geom.PointF32, size geom.PointF32, margin SideLengths) geom.RectangleF32 { + var left = margin.Left.Value() + var top = margin.Top.Value() + var w, hor = sumLengths(left, size.X, margin.Right.Value()) + var h, ver = sumLengths(top, size.Y, margin.Bottom.Value()) + var rem = geom.PtF32(geom.Max32(0, bounds.X-w), geom.Max32(0, bounds.Y-h)) + if geom.IsNaN32(left) { + left = rem.X / float32(hor) + } else if rem.X == 0 { + left = (left * bounds.X) / w + } + if geom.IsNaN32(size.X) { + size.X = rem.X / float32(hor) + } else if rem.X == 0 { + size.X = (size.X * bounds.X) / w + } else if hor == 0 { // nothing stretches + size.X += rem.X + } + if geom.IsNaN32(top) { + top = rem.Y / float32(ver) + } else if rem.Y == 0 { + top = (top * bounds.Y) / h + } + if geom.IsNaN32(size.Y) { + size.Y = rem.Y / float32(ver) + } else if rem.Y == 0 { + size.Y = (size.Y * bounds.Y) / h + } else if ver == 0 { // nothing stretches + size.Y += rem.Y + } + return geom.RectF32(left, top, left+size.X, top+size.Y) +} + +func (s *Spacing) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) { + var size = s.Proxy.DesiredSize(ctx) + var w, h = s.Width.Zero(size.X), s.Height.Zero(size.Y) + var content = addMargin(geom.PtF32(bounds.Dx(), bounds.Dy()), geom.PtF32(w.Value(), h.Value()), s.Margin).Add(bounds.Min) + s.Proxy.Arrange(ctx, content, offset) +} + +func (s *Spacing) Center() { + s.Margin.Left = Infinite() + s.Margin.Top = Infinite() + s.Margin.Right = Infinite() + s.Margin.Bottom = Infinite() +} + +func (s *Spacing) DesiredSize(ctx Context) geom.PointF32 { + var size = s.Proxy.DesiredSize(ctx) + var w, h = s.Width.Zero(size.X), s.Height.Zero(size.Y) + return geom.PtF32( + s.Margin.Left.Value()+w.Value()+s.Margin.Right.Value(), + s.Margin.Top.Value()+h.Value()+s.Margin.Bottom.Value()) +} + +func (s *Spacing) SetSize(w, h float32) { + s.Width = Fixed(w) + s.Height = Fixed(h) +} diff --git a/ui/spacing_test.go b/ui/spacing_test.go new file mode 100644 index 0000000..a321f98 --- /dev/null +++ b/ui/spacing_test.go @@ -0,0 +1,62 @@ +package ui + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "opslag.de/schobers/geom" +) + +var _ Control = &Mock{} + +type Mock struct { + ControlBase + + Size *geom.PointF32 +} + +func (m *Mock) DesiredSize(ctx Context) geom.PointF32 { + if m.Size != nil { + return *m.Size + } + return m.ControlBase.DesiredSize(ctx) +} + +func TestNoStretchFillsAvailableSpace(t *testing.T) { + m := &Mock{} + s := BuildSpacing(m, func(s *Spacing) { + s.SetSize(0, 0) + }) + s.Arrange(nil, geom.RectF32(31, 37, 73, 79), geom.ZeroPtF32) + assert.Equal(t, geom.RectF32(31, 37, 73, 79), m.bounds) +} + +func TestStretch(t *testing.T) { + m := &Mock{} + s := BuildSpacing(m, func(s *Spacing) { + s.SetSize(geom.NaN32(), geom.NaN32()) + }) + s.Arrange(nil, geom.RectF32(31, 37, 73, 79), geom.ZeroPtF32) + assert.Equal(t, geom.RectF32(31, 37, 73, 79), m.bounds) +} + +func TestCenter(t *testing.T) { + m := &Mock{Size: &geom.PointF32{X: 2, Y: 3}} + s := BuildSpacing(m, func(s *Spacing) { + s.Center() + }) + s.Arrange(nil, geom.RectF32(31, 37, 73, 79), geom.ZeroPtF32) + assert.Equal(t, geom.RectF32(51, 56.5, 53, 59.5), m.bounds) +} + +func TestFixedMargin(t *testing.T) { + m := &Mock{Size: &geom.PointF32{X: geom.NaN32(), Y: geom.NaN32()}} + s := BuildSpacing(m, func(s *Spacing) { + s.Margin.Left = Fixed(7) + s.Margin.Top = Fixed(2) + s.Margin.Right = Fixed(3) + s.Margin.Bottom = Fixed(5) + }) + s.Arrange(nil, geom.RectF32(31, 37, 73, 79), geom.ZeroPtF32) + assert.Equal(t, geom.RectF32(38, 39, 70, 74), m.bounds) +} diff --git a/ui/stretch.go b/ui/stretch.go index 6177cfa..0d52322 100644 --- a/ui/stretch.go +++ b/ui/stretch.go @@ -1,33 +1,14 @@ package ui -import ( - "opslag.de/schobers/geom" -) - -var _ Control = &stretch{} - -type stretch struct { - Proxy - - w, h bool -} - -func (s *stretch) DesiredSize(ctx Context) geom.PointF32 { - var size = geom.PtF32(geom.NaN32(), geom.NaN32()) - if !s.w || !s.h { - child := s.Content.DesiredSize(ctx) - if !s.w { - size.X = child.X +func newStretch(c Control, w, h bool) *Spacing { + return BuildSpacing(c, func(s *Spacing) { + if w { + s.Width = Infinite() } - if !s.h { - size.Y = child.Y + if h { + s.Height = Infinite() } - } - return size -} - -func newStretch(c Control, w, h bool) *stretch { - return &stretch{Proxy{Content: c}, w, h} + }) } // Stretch wraps the supplied control to stretch in both directions.