~eliasnaur/gio-patches

gio: layout: add Grid and Table support v1 PROPOSED

~pierrec
~pierrec: 1
 layout: add Grid and Table support

 1 files changed, 259 insertions(+), 0 deletions(-)
Le mer. 2 déc. 2020 à 14:59, Elias Naur <mail@eliasnaur.com> a écrit :
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/15408/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio] layout: add Grid and Table support Export this patch

~pierrec
From: Pierre Curto <pierre.curto@gmail.com>

Being able to place graphical elements in a grid fashion is fairly common and currently lacking in Gio.
This patch is an attempt to fill that gap with the following new types:
- Grid: display a given number of widgets along an axis, with the ability to scroll on the cross axis
- GriwWrap: display widgets along an axis and going to the cross axis when room runs out. No scrolling.
- Table: display widgets on both axis, given the number of them on each. Scrollable on both axis.
Signed-off-by: pierre <pierre.curto@gmail.com>
---
 layout/grid.go | 259 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 259 insertions(+)
 create mode 100644 layout/grid.go

diff --git a/layout/grid.go b/layout/grid.go
new file mode 100644
index 0000000..fae6bc5
--- /dev/null
+++ b/layout/grid.go
@@ -0,0 +1,259 @@
// SPDX-License-Identifier: Unlicense OR MIT

package layout

import (
	"image"

	"gioui.org/f32"
	"gioui.org/io/event"
	"gioui.org/op"
	"gioui.org/unit"
)

// GridElement lays out the ith element of a Grid.
type GridElement func(gtx Context, i int) Dimensions

// TableElement lays out the Table element located at column x and row y.
type TableElement func(gtx Context, x, y int) Dimensions

// Grid lays out at most Num elements along the main axis.
// The number of cross axis elements depend on the total number of elements.
type Grid struct {
	Num       int
	Axis      Axis
	Alignment Alignment
	list      List
}

// GridWrap lays out as many elements as possible along the main axis
// before wrapping to the cross axis.
type GridWrap struct {
	Axis      Axis
	Alignment Alignment
}

// Table lays out element by their coordinates.
// All elements within a column have the same width, and
// the same height within a row.
type Table struct {
	// CellSize returns the size for the cell located at column x and row y.
	CellSize     func(m unit.Metric, x, y int) image.Point
	xList, yList List
	x, y         int
}

type wrapData struct {
	dims Dimensions
	call op.CallOp
}

func (g GridWrap) Layout(gtx Context, num int, el GridElement) Dimensions {
	defer op.Push(gtx.Ops).Pop()
	csMax := gtx.Constraints.Max
	var mainSize, crossSize, mainPos, crossPos, base int
	gtx.Constraints.Min = image.Point{}
	mainCs := axisMain(g.Axis, csMax)
	crossCs := axisCross(g.Axis, gtx.Constraints.Max)

	var els []wrapData
	for i := 0; i < num; i++ {
		macro := op.Record(gtx.Ops)
		dims, okMain, okCross := g.place(gtx, i, el)
		call := macro.Stop()
		if !okMain && !okCross {
			break
		}
		main := axisMain(g.Axis, dims.Size)
		cross := axisCross(g.Axis, dims.Size)
		if okMain {
			els = append(els, wrapData{dims, call})

			mainCs := axisMain(g.Axis, gtx.Constraints.Max)
			gtx.Constraints.Max = axisPoint(g.Axis, mainCs-main, crossCs)

			mainPos += main
			crossPos = max(crossPos, cross)
			base = max(base, dims.Baseline)
			continue
		}
		// okCross
		mainSize = max(mainSize, mainPos)
		crossSize += crossPos
		g.placeAll(gtx.Ops, els, crossPos, base)
		els = append(els[:0], wrapData{dims, call})

		gtx.Constraints.Max = axisPoint(g.Axis, mainCs-main, crossCs-crossPos)
		mainPos = main
		crossPos = cross
		base = dims.Baseline
	}
	mainSize = max(mainSize, mainPos)
	crossSize += crossPos
	g.placeAll(gtx.Ops, els, crossPos, base)
	sz := axisPoint(g.Axis, mainSize, crossSize)
	return Dimensions{Size: sz}
}

func (g GridWrap) place(gtx Context, i int, el GridElement) (dims Dimensions, okMain, okCross bool) {
	cs := gtx.Constraints
	if g.Axis == Horizontal {
		gtx.Constraints.Max.X = inf
	} else {
		gtx.Constraints.Max.Y = inf
	}
	dims = el(gtx, i)
	okMain = dims.Size.X <= cs.Max.X
	okCross = dims.Size.Y <= cs.Max.Y
	if g.Axis == Vertical {
		okMain, okCross = okCross, okMain
	}
	return
}

func (g GridWrap) placeAll(ops *op.Ops, els []wrapData, crossMax, baseMax int) {
	var mainPos int
	var pt image.Point
	for i, el := range els {
		cross := axisCross(g.Axis, el.dims.Size)
		switch g.Alignment {
		case Start:
			cross = 0
		case End:
			cross = crossMax - cross
		case Middle:
			cross = (crossMax - cross) / 2
		case Baseline:
			if g.Axis == Horizontal {
				cross = baseMax - el.dims.Baseline
			} else {
				cross = 0
			}
		}
		if cross == 0 {
			el.call.Add(ops)
		} else {
			pt = axisPoint(g.Axis, 0, cross)
			op.Offset(FPt(pt)).Add(ops)
			el.call.Add(ops)
			op.Offset(FPt(pt.Mul(-1))).Add(ops)
		}
		if i == len(els)-1 {
			pt = axisPoint(g.Axis, -mainPos, crossMax)
		} else {
			main := axisMain(g.Axis, el.dims.Size)
			pt = axisPoint(g.Axis, main, 0)
			mainPos += main
		}
		op.Offset(FPt(pt)).Add(ops)
	}
}

func (g *Grid) Layout(gtx Context, num int, el GridElement) Dimensions {
	if g.Num == 0 {
		return Dimensions{Size: gtx.Constraints.Min}
	}
	if g.Axis == g.list.Axis {
		if g.Axis == Horizontal {
			g.list.Axis = Vertical
		} else {
			g.list.Axis = Horizontal
		}
		g.list.Alignment = g.Alignment
	}
	csMax := gtx.Constraints.Max
	return g.list.Layout(gtx, (num+g.Num-1)/g.Num, func(gtx Context, idx int) Dimensions {
		defer op.Push(gtx.Ops).Pop()
		if g.Axis == Horizontal {
			gtx.Constraints.Max.X = inf
		} else {
			gtx.Constraints.Max.Y = inf
		}
		gtx.Constraints.Min = image.Point{}
		var mainMax, crossMax int
		left := axisMain(g.Axis, csMax)
		i := idx * g.Num
		n := min(num, i+g.Num)
		for ; i < n; i++ {
			dims := el(gtx, i)
			main := axisMain(g.Axis, dims.Size)
			crossMax = max(crossMax, axisCross(g.Axis, dims.Size))
			left -= main
			if left <= 0 {
				mainMax = axisMain(g.Axis, csMax)
				break
			}
			pt := axisPoint(g.Axis, main, 0)
			op.Offset(FPt(pt)).Add(gtx.Ops)
			mainMax += main
		}
		return Dimensions{Size: axisPoint(g.Axis, mainMax, crossMax)}
	})
}

func (t *Table) Layout(gtx Context, xn, yn int, el TableElement) Dimensions {
	defer op.Push(gtx.Ops).Pop()
	csMax := gtx.Constraints.Max

	// Deliver the same events for both lists.
	listContext := gtx
	evs := gtx.Events(&t.yList.scroll)

	listContext.Queue = queue(evs)
	t.xList.Axis = Horizontal
	t.xList.Layout(listContext, xn, func(gtx Context, x int) Dimensions {
		sz := t.CellSize(gtx.Metric, x, t.y)
		return Dimensions{Size: image.Point{X: sz.X, Y: csMax.Y}}
	})
	t.x = t.xList.Position.First

	listContext.Queue = queue(evs)
	t.yList.Axis = Vertical
	t.yList.Layout(listContext, yn, func(gtx Context, y int) Dimensions {
		sz := t.CellSize(gtx.Metric, t.x, y)
		return Dimensions{Size: image.Point{X: csMax.X, Y: sz.Y}}
	})
	t.y = t.yList.Position.First

	gtx.Constraints.Min = image.Point{}
	var yy int
	for y := t.y; y < yn; y++ {
		var xx int
		var sz image.Point
		for x := t.x; x < xn; x++ {
			sz = t.CellSize(gtx.Metric, x, y)
			gtx.Constraints.Max = sz
			el(gtx, x, y)
			if xx >= csMax.X {
				yy += sz.Y
				break
			}
			xx += sz.X
			op.Offset(f32.Point{X: float32(sz.X)}).Add(gtx.Ops)
		}
		if yy >= csMax.Y {
			break
		}
		pt := image.Point{X: -xx, Y: sz.Y}
		op.Offset(FPt(pt)).Add(gtx.Ops)
	}
	return Dimensions{Size: csMax}
}

type queue []event.Event

func (q queue) Events(_ event.Tag) []event.Event { return q }

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
-- 
2.26.2
CC: Chris Waldon <christopher.waldon.dev@gmail.com>

On Tue Dec 1, 2020 at 11:51, ~pierrec wrote: