From 8a7b2832df7a3064e01b0e2feeebe2a340f3fea2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 20 Aug 2023 17:41:14 -0400 Subject: [PATCH] Add grid layout --- layouts/grid.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ slider.go | 4 +- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 layouts/grid.go diff --git a/layouts/grid.go b/layouts/grid.go new file mode 100644 index 0000000..d09d6aa --- /dev/null +++ b/layouts/grid.go @@ -0,0 +1,108 @@ +package layouts + +import "math" +import "image" +import "git.tebibyte.media/tomo/tomo" + +// Grid is a layout that arranges boxes in a grid formation with distinct rows +// and columns. It is great for creating forms. +type Grid struct { + xExpand []bool + yExpand []bool +} + +// NewGrid creates a new grid layout. Rows and columns are specified as slices +// of booleans, where true means a row or column will expand and false means it +// will contract. Boxes are laid out left to right, then top to bottom. Boxes +// that go beyond the lengh of rows will be laid out according to columns, but +// they will not expand vertically. +func NewGrid (columns, rows []bool) *Grid { + this := &Grid { + xExpand: columns, + yExpand: rows, + } + return this +} + +func (this *Grid) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point { + cols, rows := this.minimums(boxes) + size := image.Pt ( + (len(cols) - 1) * hints.Gap.X, + (len(rows) - 1) * hints.Gap.Y) + for _, width := range cols { size.X += width } + for _, height := range rows { size.Y += height } + return size +} + +func (this *Grid) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { + xExpand := func (index int) bool { + return this.xExpand[index] + } + yExpand := func (index int) bool { + if index < len(this.yExpand) { return this.yExpand[index] } + return false + } + + cols, rows := this.minimums(boxes) + expand(hints, cols, hints.Bounds.Dx(), xExpand) + expand(hints, rows, hints.Bounds.Dy(), yExpand) + + position := hints.Bounds.Min + for index, box := range boxes { + col, row := index % len(cols), index / len(cols) + box.SetBounds(image.Rectangle { + Min: position, + Max: position.Add(image.Pt(cols[col], rows[row])), + }) + if col == len(cols) - 1 { + position.X = hints.Bounds.Min.X + position.Y += rows[row] + hints.Gap.Y + } else { + position.X += cols[col] + hints.Gap.X + } + } +} + +func (this *Grid) minimums (boxes []tomo.Box) ([]int, []int) { + nCols, nRows := this.dimensions(boxes) + cols, rows := make([]int, nCols), make([]int, nRows) + + for index, box := range boxes { + col, row := index % len(cols), index / len(cols) + minimum := box.MinimumSize() + if cols[col] < minimum.X { + cols[col] = minimum.X + } + if rows[row] < minimum.Y { + rows[row] = minimum.Y + } + } + + return cols, rows +} + +func (this *Grid) dimensions (boxes []tomo.Box) (int, int) { + return len(this.xExpand), ceilDiv(len(boxes), len(this.xExpand)) +} + +func expand (hints tomo.LayoutHints, sizes []int, space int, expands func (int) bool) { + gaps := len(sizes) - 1 + freeSpace := float64(space - hints.Gap.Y * gaps) + nExpanding := 0; for index, minimum := range sizes { + if expands(index) { + nExpanding ++ + } else { + freeSpace -= float64(minimum) + } + } + expandingSize := freeSpace / float64(nExpanding) + for index := range sizes { + if expands(index) { + sizes[index] = int(expandingSize) + } + } +} + +func ceilDiv (x, y int) int { + return int(math.Ceil(float64(x) / float64(y))) +} diff --git a/slider.go b/slider.go index 0b1ad23..1507cb0 100644 --- a/slider.go +++ b/slider.go @@ -189,7 +189,7 @@ func (this sliderLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { if this.vertical { height := gutter.Dy() - handle.Dy() offset := int(float64(height) * this.value) - gutter.Max.X = handle.Max.X + handle.Max.X = gutter.Dx() boxes[0].SetBounds ( handle. Add(image.Pt(0, offset)). @@ -197,7 +197,7 @@ func (this sliderLayout) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { } else { width := gutter.Dx() - handle.Dx() offset := int(float64(width) * this.value) - gutter.Max.Y = handle.Max.Y + handle.Max.Y = gutter.Dy() boxes[0].SetBounds ( handle. Add(image.Pt(offset, 0)).