table.go raw

   1  package gel
   2  
   3  import (
   4  	"image"
   5  	"sort"
   6  	
   7  	l "github.com/p9c/gio/layout"
   8  	"github.com/p9c/gio/op"
   9  )
  10  
  11  type Cell struct {
  12  	l.Widget
  13  	dims     l.Dimensions
  14  	computed bool
  15  	// priority only has meaning for the header row in defining an order of eliminating elements to fit a width.
  16  	// When trimming size to fit width add from highest to lowest priority and stop when dimensions exceed the target.
  17  	Priority int
  18  }
  19  
  20  func (c *Cell) getWidgetDimensions(gtx l.Context) {
  21  	if c.Widget == nil {
  22  		// this happens when new items are added if a frame reads the cell, it just - can't - be rendered!
  23  		return
  24  	}
  25  	if c.computed {
  26  		return
  27  	}
  28  	// gather the dimensions of the list elements
  29  	gtx.Ops.Reset()
  30  	child := op.Record(gtx.Ops)
  31  	c.dims = c.Widget(gtx)
  32  	c.computed = true
  33  	_ = child.Stop()
  34  	return
  35  }
  36  
  37  type CellRow []Cell
  38  
  39  func (c CellRow) GetPriority() (out CellPriorities) {
  40  	for i := range c {
  41  		var cp CellPriority
  42  		cp.Priority = c[i].Priority
  43  		cp.Column = i
  44  		out = append(out, cp)
  45  	}
  46  	sort.Sort(out)
  47  	return
  48  }
  49  
  50  type CellPriority struct {
  51  	Column   int
  52  	Priority int
  53  }
  54  
  55  type CellPriorities []CellPriority
  56  
  57  // Len sorts a cell row by priority
  58  func (c CellPriorities) Len() int {
  59  	return len(c)
  60  }
  61  func (c CellPriorities) Less(i, j int) bool {
  62  	return c[i].Priority < c[j].Priority
  63  }
  64  func (c CellPriorities) Swap(i, j int) {
  65  	c[i], c[j] = c[j], c[i]
  66  }
  67  
  68  type CellGrid []CellRow
  69  
  70  // Table is a super simple table widget that finds the dimensions of all cells, sets all to max of each axis, and then
  71  // scales the remaining space evenly
  72  type Table struct {
  73  	*Window
  74  	header           CellRow
  75  	body             CellGrid
  76  	list             *List
  77  	Y, X             []int
  78  	headerBackground string
  79  	cellBackground   string
  80  	reverse          bool
  81  }
  82  
  83  func (w *Window) Table() *Table {
  84  	return &Table{
  85  		Window: w,
  86  		list:   w.List(),
  87  	}
  88  }
  89  
  90  func (t *Table) SetReverse(color string) *Table {
  91  	t.reverse = true
  92  	return t
  93  }
  94  
  95  func (t *Table) HeaderBackground(color string) *Table {
  96  	t.headerBackground = color
  97  	return t
  98  }
  99  
 100  func (t *Table) CellBackground(color string) *Table {
 101  	t.cellBackground = color
 102  	return t
 103  }
 104  
 105  func (t *Table) Header(h CellRow) *Table {
 106  	t.header = h
 107  	return t
 108  }
 109  
 110  func (t *Table) Body(g CellGrid) *Table {
 111  	t.body = g
 112  	return t
 113  }
 114  
 115  func (t *Table) Fn(gtx l.Context) l.Dimensions {
 116  	// D.Ln(len(t.body), len(t.header))
 117  	if len(t.header) == 0 {
 118  		return l.Dimensions{}
 119  	}
 120  	// if len(t.body) == 0 || len(t.header) == 0 {
 121  	// 	return l.Dimensions{}
 122  	// }
 123  	
 124  	for i := range t.body {
 125  		if len(t.header) != len(t.body[i]) {
 126  			// this should never happen hence panic
 127  			panic("not all rows are equal number of cells")
 128  		}
 129  	}
 130  	gtx1 := CopyContextDimensionsWithMaxAxis(gtx, l.Vertical)
 131  	gtx1.Constraints.Max = image.Point{X: Inf, Y: Inf}
 132  	// gather the dimensions from all cells
 133  	for i := range t.header {
 134  		t.header[i].getWidgetDimensions(gtx1)
 135  	}
 136  	// D.S(t.header)
 137  	for i := range t.body {
 138  		for j := range t.body[i] {
 139  			t.body[i][j].getWidgetDimensions(gtx1)
 140  		}
 141  	}
 142  	// D.S(t.body)
 143  	
 144  	// find the max of each row and column
 145  	var table CellGrid
 146  	table = append(table, t.header)
 147  	table = append(table, t.body...)
 148  	t.Y = make([]int, len(table))
 149  	t.X = make([]int, len(table[0]))
 150  	for i := range table {
 151  		for j := range table[i] {
 152  			y := table[i][j].dims.Size.Y
 153  			if y > t.Y[i] {
 154  				t.Y[i] = y
 155  			}
 156  			x := table[i][j].dims.Size.X
 157  			if x > t.X[j] {
 158  				t.X[j] = x
 159  			}
 160  		}
 161  	}
 162  	// // D.S(t.Y)
 163  	// D.S(t.X)
 164  	var total int
 165  	for i := range t.X {
 166  		total += t.X[i]
 167  	}
 168  	// D.S(t.X)
 169  	// D.Ln(total)
 170  	maxWidth := gtx.Constraints.Max.X
 171  	for i := range t.X {
 172  		t.X[i] = int(float32(t.X[i]) * float32(maxWidth) / float32(total))
 173  	}
 174  	// D.S(t.X)
 175  	// D.Ln(maxWidth)
 176  	// // find the columns that will be rendered into the existing width
 177  	// // D.S(t.header)
 178  	// priorities := t.header.GetPriority()
 179  	// // D.S(priorities)
 180  	// var runningTotal, prev int
 181  	// columnsToRender := make([]int, 0)
 182  	// for i := range priorities {
 183  	// 	prev = runningTotal
 184  	// 	x := t.header[priorities[i].Column].dims.Size.X
 185  	// 	// D.Ln(priorities[i], x)
 186  	// 	runningTotal += x
 187  	//
 188  	// 	if runningTotal > maxWidth {
 189  	// 		// D.Ln(runningTotal, prev, maxWidth)
 190  	// 		break
 191  	// 	}
 192  	// 	columnsToRender = append(columnsToRender, priorities[i].Column)
 193  	// }
 194  	// // txsort the columns to render into their original order
 195  	// txsort.Ints(columnsToRender)
 196  	// // D.S(columnsToRender)
 197  	// // D.Ln(len(columnsToRender))
 198  	// // All fields will be expanded by the following ratio to reach the target width
 199  	// expansionFactor := float32(maxWidth) / float32(prev)
 200  	// outColWidths := make([]int, len(columnsToRender))
 201  	// for i := range columnsToRender {
 202  	// 	outColWidths[i] = int(float32(t.X[columnsToRender[i]]) * expansionFactor)
 203  	// }
 204  	// // D.Ln(outColWidths)
 205  	// // assemble the grid to be rendered as a two dimensional slice
 206  	// grid := make([][]l.Widget, len(t.body)+1)
 207  	// for i := 0; i < len(columnsToRender); i++ {
 208  	// 	grid[0] = append(grid[0], t.header[columnsToRender[i]].Widget)
 209  	// }
 210  	// // for i := 0; i < len(columnsToRender); i++ {
 211  	// // 	for j := range t.body[i] {
 212  	// // 		grid[i+1] = append(grid[i+1], t.body[i][j].Widget)
 213  	// // 	}
 214  	// // }
 215  	// // D.S(grid)
 216  	// // assemble each row into a flex
 217  	// out := make([]l.Widget, len(grid))
 218  	// for i := range grid {
 219  	// 	outFlex := t.Theme.Flex()
 220  	// 	for jj, j := range grid[i] {
 221  	// 		x := j
 222  	// 		_ = jj
 223  	// 		// outFlex.Rigid(x)
 224  	// 		outFlex.Rigid(func(gtx l.Context) l.Dimensions {
 225  	// 			// lock the cell to the calculated width.
 226  	// 			gtx.Constraints.Max.X = outColWidths[jj]
 227  	// 			gtx.Constraints.Min.X = gtx.Constraints.Max.X
 228  	// 			return x(gtx)
 229  	// 		})
 230  	// 	}
 231  	// 	out[i] = outFlex.Fn
 232  	// }
 233  	header := t.Theme.Flex() // .SpaceEvenly()
 234  	for x, oi := range t.header {
 235  		i := x
 236  		// header is not in the list but drawn above it
 237  		oie := oi
 238  		txi := t.X[i]
 239  		tyi := t.Y[0]
 240  		header.Rigid(func(gtx l.Context) l.Dimensions {
 241  			cs := gtx.Constraints
 242  			cs.Max.X = txi
 243  			cs.Min.X = gtx.Constraints.Max.X
 244  			cs.Max.Y = tyi
 245  			cs.Min.Y = gtx.Constraints.Max.Y
 246  			// gtx.Constraints.Constrain(image.Point{X: txi, Y: tyi})
 247  			dims := t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, EmptySpace(txi, tyi)).Fn(gtx)
 248  			oie.Widget(gtx)
 249  			return dims
 250  		})
 251  	}
 252  	
 253  	var out CellGrid
 254  	out = CellGrid{t.header}
 255  	if t.reverse {
 256  		// append the body elements in reverse order stored
 257  		lb := len(t.body) - 1
 258  		for i := range t.body {
 259  			out = append(out, t.body[lb-i])
 260  		}
 261  	} else {
 262  		out = append(out, t.body...)
 263  	}
 264  	le := func(gtx l.Context, index int) l.Dimensions {
 265  		f := t.Theme.Flex() // .SpaceEvenly()
 266  		oi := out[index]
 267  		for x, oiee := range oi {
 268  			i := x
 269  			if index == 0 {
 270  				// we skip the header, not implemented but the header could be part of the scrollable area if need
 271  				// arises later, unwrap this block on a flag
 272  			} else {
 273  				if index >= len(t.Y) {
 274  					break
 275  				}
 276  				oie := oiee
 277  				txi := t.X[i]
 278  				tyi := t.Y[index]
 279  				f.Rigid(t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions {
 280  					cs := gtx.Constraints
 281  					cs.Max.X = txi
 282  					cs.Min.X = gtx.Constraints.Max.X
 283  					cs.Max.Y = tyi
 284  					cs.Min.Y = gtx.Constraints.Max.Y // gtx.Constraints.Constrain(image.Point{
 285  					// 	X: t.X[i],
 286  					// 	Y: t.Y[index],
 287  					// })
 288  					gtx.Constraints.Max.X = txi
 289  					// gtx.Constraints.Min.X = gtx.Constraints.Max.X
 290  					gtx.Constraints.Max.Y = tyi
 291  					// gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
 292  					dims := EmptySpace(txi, tyi)(gtx)
 293  					// dims
 294  					oie.Widget(gtx)
 295  					return dims
 296  				}).Fn)
 297  			}
 298  		}
 299  		return f.Fn(gtx)
 300  	}
 301  	return t.Theme.VFlex().
 302  		Rigid(func(gtx l.Context) l.Dimensions {
 303  			// header is fixed to the top of the widget
 304  			return t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, header.Fn).Fn(gtx)
 305  		}).
 306  		Flexed(1,
 307  			t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions {
 308  				return t.list.Vertical().
 309  					Length(len(out)).
 310  					Background(t.cellBackground).
 311  					ListElement(le).
 312  					Fn(gtx)
 313  			}).Fn,
 314  		).
 315  		Fn(gtx)
 316  }
 317