Skip to content

Instantly share code, notes, and snippets.

@thetooth

thetooth/cell.go Secret

Created November 10, 2020 02:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thetooth/c04ea3d572ed554fb87d98e830e11921 to your computer and use it in GitHub Desktop.
Save thetooth/c04ea3d572ed554fb87d98e830e11921 to your computer and use it in GitHub Desktop.
package datatable
import (
"fmt"
"gioui.org/layout"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
// A cell must provide a means of rendering(Layout), aligning itself in a column(AlignTo)
type Cell interface {
Layout(C) D
Base() *BasicCell
AlignTo(text.Alignment)
}
// NewCell returns a BasicCell which can be used directly or be built on top of
func (s *DataTable) NewCell(content interface{}) *BasicCell {
t := material.Body1(s.th, "")
return &BasicCell{LabelStyle: t, Value: content}
}
// NewHeader returns a Header cell which contains sort controls for a given column
func (s *DataTable) NewHeader(title string, size float32, align text.Alignment) *Header {
base := s.NewCell(title)
base.Font.Weight = text.Bold
base.Alignment = align
h := &Header{BasicCell: base, Size: size, Alignment: align}
h.sortBtn = material.IconButton(s.th, &h.sort, iconSort)
h.sortBtn.Background = s.th.Color.InvText
h.sortBtn.Color = s.th.Color.Hint
h.sortBtn.Inset = layout.UniformInset(unit.Dp(4))
return h
}
// BasicCell provides simple text rendering of Value
type BasicCell struct {
material.LabelStyle
Value interface{}
}
func (s *BasicCell) Base() *BasicCell {
return s
}
func (s *BasicCell) Layout(gtx C) D {
s.LabelStyle.Text = fmt.Sprintf("%v", s.Value)
return s.LabelStyle.Layout(gtx)
}
func (s *BasicCell) AlignTo(align text.Alignment) {
s.Alignment = align
}
// Header provides text rendering and sorting of columns
type Header struct {
*BasicCell
Size float32
Alignment text.Alignment
sort widget.Clickable
sortBtn material.IconButtonStyle
sortInvert bool
}
package cui
import (
"image"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
)
// Hoverable tracks mouse hovers over some area.
// Hoverable areas are also clickable, for convenience.
//
// Events are passed through so hoverable can be stacked on top of other widgets
// without interfering with them.
type Hoverable struct {
widget.Clickable
hovered bool
}
// Hovered if mouse has entered the area.
func (h *Hoverable) Hovered() bool {
return h.hovered
}
// Layout Hoverable according to min constraints.
func (h *Hoverable) Layout(gtx layout.Context, dims image.Rectangle) layout.Dimensions {
{
stack := op.Push(gtx.Ops)
pointer.PassOp{Pass: true}.Add(gtx.Ops)
//image.Rectangle{Max: gtx.Constraints.Min}
pointer.Rect(dims).Add(gtx.Ops)
h.Clickable.Layout(gtx)
stack.Pop()
}
h.update(gtx)
{
stack := op.Push(gtx.Ops)
pointer.PassOp{Pass: true}.Add(gtx.Ops)
pointer.Rect(dims).Add(gtx.Ops)
pointer.InputOp{
Tag: h,
Types: pointer.Enter | pointer.Leave | pointer.Cancel,
}.Add(gtx.Ops)
stack.Pop()
}
return layout.Dimensions{Size: dims.Max}
}
func (h *Hoverable) update(gtx layout.Context) {
for _, event := range gtx.Events(h) {
if event, ok := event.(pointer.Event); ok {
switch event.Type {
case pointer.Enter:
h.hovered = true
case pointer.Leave, pointer.Cancel:
h.hovered = false
}
}
}
}
package datatable
import (
"fmt"
"image"
"image/color"
"reflect"
"sort"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
D = layout.Dimensions
C = layout.Context
)
func New(th *material.Theme) *DataTable {
table := DataTable{
Data: []Row{},
SelectAll: &widget.Bool{},
RowsPerPage: 5,
th: th,
list: &layout.List{
Axis: layout.Vertical,
},
back: &widget.Clickable{},
fback: &widget.Clickable{},
forwards: &widget.Clickable{},
fforwards: &widget.Clickable{},
}
return &table
}
type DataTable struct {
Data []Row
Header Row
SelectAll *widget.Bool
list *layout.List
page int
RowsPerPage int
back, forwards *widget.Clickable
fback, fforwards *widget.Clickable
th *material.Theme
}
func (s *DataTable) AppendRows(r ...Row) {
s.Data = append(s.Data, r...)
}
func (s *DataTable) Selected() (rows []Row) {
for _, row := range s.Data {
if row.Selected.Value {
rows = append(rows, row)
}
}
return
}
func (s *DataTable) SortBy(column int, invert bool) {
if header, ok := s.Header.Columns[column].(*Header); ok {
// Deactivate other columns sort indicator
for _, cell := range s.Header.Columns {
if otherHeader, ok := cell.(*Header); ok && otherHeader != header {
otherHeader.sortBtn.Color = s.th.Color.Hint
otherHeader.sortBtn.Icon = iconSort
otherHeader.sortInvert = false
}
}
// Show this column is sorting
header.sortBtn.Color = s.th.Color.Primary
header.sortInvert = !header.sortInvert
if header.sortInvert {
header.sortBtn.Icon = iconSortAZ
} else {
header.sortBtn.Icon = iconSortZA
}
}
// Perform sort
sort.Slice(s.Data, func(i, j int) bool {
a := s.Data[i].Columns[column].Base().Value
b := s.Data[j].Columns[column].Base().Value
weight := false
if reflect.TypeOf(a).Kind() != reflect.TypeOf(b).Kind() {
fmt.Println("Warning: rows in a single column have differing types")
return false
}
switch a := a.(type) {
case string:
weight = a < s.Data[j].Columns[column].Base().Value.(string)
case int:
weight = a < s.Data[j].Columns[column].Base().Value.(int)
case float32:
weight = a < s.Data[j].Columns[column].Base().Value.(float32)
case float64:
weight = a < s.Data[j].Columns[column].Base().Value.(float64)
}
// XOR against sort inversion
return invert != weight
})
}
func (s *DataTable) Layout(gtx C) D {
widgets := []layout.Widget{}
widgets = append(widgets, func(gtx C) D {
// Select all button
cells := []layout.FlexChild{
layout.Rigid(func(gtx C) D {
c := material.CheckBox(s.th, s.SelectAll, "")
if s.SelectAll.Changed() {
for _, row := range s.Data {
row.Selected.Value = s.SelectAll.Value
}
}
return layout.UniformInset(unit.Dp(8)).Layout(gtx, c.Layout)
}),
}
// Headers, k is the column for sorting feature
for id, cell := range s.Header.Columns {
// Local copy due to annon function
lcell := cell
k := id
if header, ok := lcell.(*Header); ok {
if header.Size != 0 {
cells = append(cells, layout.Flexed(header.Size, func(gtx C) D {
// Spacing is inverse of text alignment
spacing := layout.SpaceEnd
switch header.Alignment {
case text.Middle:
spacing = layout.SpaceSides
case text.End:
spacing = layout.SpaceStart
}
// Draw the sort button and header text
return layout.Flex{Axis: layout.Horizontal, Spacing: spacing, Alignment: layout.Middle}.Layout(gtx,
// Sort button
layout.Rigid(func(gtx C) D {
if header.sort.Clicked() {
s.SortBy(k, header.sortInvert)
}
return header.sortBtn.Layout(gtx)
}),
// Cell content
layout.Rigid(lcell.Layout),
)
}))
} else {
cells = append(cells, layout.Rigid(cell.Layout))
}
}
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, cells...)
})
// Calculate pagination slice values
page := s.page + 1
end := page * s.RowsPerPage
start := end - s.RowsPerPage
// Extract from slice
sub := s.Data[start:]
if len(sub) >= end-start {
sub = sub[:s.RowsPerPage]
}
// Data rows
for _, row := range sub {
// Local copy because we are using annonamous functions
lrow := row
widgets = append(widgets, func(gtx C) D {
// Background highlight
if lrow.hover.Hovered() || lrow.Selected.Value {
paint.ColorOp{Color: color.RGBA{R: 245, G: 245, B: 245, A: 0xFF}}.Add(gtx.Ops)
paint.PaintOp{Rect: f32.Rect(0, -8, float32(gtx.Constraints.Max.X)+16, float32(gtx.Constraints.Max.Y))}.Add(gtx.Ops)
}
cells := []layout.FlexChild{
layout.Rigid(func(gtx C) D {
c := material.CheckBox(s.th, lrow.Selected, "")
return layout.UniformInset(unit.Dp(8)).Layout(gtx, c.Layout)
}),
}
for j, cell := range lrow.Columns {
w := float32(0)
if len(s.Header.Columns) > j {
header, ok := s.Header.Columns[j].(*Header)
if ok {
w = header.Size
cell.AlignTo(header.Alignment)
}
}
if w != 0 {
cells = append(cells, layout.Flexed(w, cell.Layout))
} else {
cells = append(cells, layout.Rigid(cell.Layout))
}
}
return layout.Flex{Alignment: layout.Middle}.Layout(gtx, cells...)
})
}
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return s.list.Layout(gtx, len(widgets), func(gtx C, i int) D {
d := layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8)}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Inset{Right: unit.Dp(16)}.Layout(gtx, widgets[i])
}),
layout.Rigid(func(gtx C) D {
paint.ColorOp{Color: color.RGBA{R: 238, G: 238, B: 238, A: 0xFF}}.Add(gtx.Ops)
paint.PaintOp{Rect: f32.Rect(0, 8, float32(gtx.Constraints.Max.X), unit.Dp(1).V+8)}.Add(gtx.Ops)
return layout.Dimensions{Size: image.Point{gtx.Constraints.Max.X, 1}}
}),
)
})
// 0 is the header
if i != 0 {
s.Data[i-1].hover.Layout(gtx, image.Rectangle{image.Point{0, 0}, image.Point{X: gtx.Constraints.Max.X, Y: d.Size.Y}})
}
return d
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8)}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle, Spacing: layout.SpaceStart}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Dimensions{}
}),
layout.Rigid(func(gtx C) D {
pages := material.Body1(s.th, fmt.Sprintf("%d-%d of %d", start+1, start+len(sub), len(s.Data)))
return layout.Inset{Right: unit.Dp(32), Top: unit.Dp(4), Bottom: unit.Dp(4)}.Layout(gtx, pages.Layout)
}),
layout.Rigid(func(gtx C) D {
btn := material.IconButton(s.th, s.fback, iconFLeft)
btn.Background = s.th.Color.InvText
btn.Color = s.th.Color.Text
btn.Inset = layout.UniformInset(unit.Dp(4))
if s.fback.Clicked() {
s.page = 0
}
return layout.Inset{Right: unit.Dp(8)}.Layout(gtx, btn.Layout)
}),
layout.Rigid(func(gtx C) D {
btn := material.IconButton(s.th, s.back, iconLeft)
btn.Background = s.th.Color.InvText
btn.Color = s.th.Color.Text
btn.Inset = layout.UniformInset(unit.Dp(4))
if s.back.Clicked() {
s.page--
if s.page < 0 {
s.page = 0
}
}
return layout.Inset{Right: unit.Dp(8)}.Layout(gtx, btn.Layout)
}),
layout.Rigid(func(gtx C) D {
btn := material.IconButton(s.th, s.forwards, iconRight)
btn.Background = s.th.Color.InvText
btn.Color = s.th.Color.Text
btn.Inset = layout.UniformInset(unit.Dp(4))
if s.forwards.Clicked() {
s.page++
if s.page*s.RowsPerPage >= len(s.Data) {
s.page--
}
}
return layout.Inset{Right: unit.Dp(8)}.Layout(gtx, btn.Layout)
}),
layout.Rigid(func(gtx C) D {
btn := material.IconButton(s.th, s.fforwards, iconFRight)
btn.Background = s.th.Color.InvText
btn.Color = s.th.Color.Text
btn.Inset = layout.UniformInset(unit.Dp(4))
if s.fforwards.Clicked() {
for (s.page+1)*s.RowsPerPage < len(s.Data) {
s.page++
}
}
return layout.Inset{Right: unit.Dp(8)}.Layout(gtx, btn.Layout)
}),
)
}),
layout.Rigid(func(gtx C) D {
paint.ColorOp{Color: color.RGBA{R: 238, G: 238, B: 238, A: 0xFF}}.Add(gtx.Ops)
paint.PaintOp{Rect: f32.Rect(0, 8, float32(gtx.Constraints.Max.X), unit.Dp(1).V+8)}.Add(gtx.Ops)
return layout.Dimensions{Size: image.Point{gtx.Constraints.Max.X, 1}}
}),
)
})
}),
)
}
package main
import (
"encoding/csv"
"fmt"
"image/color"
"io"
"io/ioutil"
"os"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/font/opentype"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
"prostocklivestock.com.au/thetooth/datatable"
)
type (
D = layout.Dimensions
C = layout.Context
)
var (
iconCheck, iconUncheck *widget.Icon
)
func init() {
var err error
iconCheck, err = widget.NewIcon(icons.ActionDone)
if err != nil {
panic(err)
}
iconUncheck, err = widget.NewIcon(icons.AlertErrorOutline)
if err != nil {
panic(err)
}
}
func ttfFontFace(file string) (text.FontFace, error) {
ttf, err := ioutil.ReadFile(file)
if err != nil {
return text.FontFace{}, err
}
fnt := text.Font{}
face, err := opentype.Parse(ttf)
if err != nil {
panic(fmt.Sprintf("failed to parse font: %v", err))
}
fnt.Typeface = "Go"
return text.FontFace{Font: fnt, Face: face}, nil
}
func rgb(c uint32) color.RGBA {
return argb(0xff000000 | c)
}
func argb(c uint32) color.RGBA {
return color.RGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)}
}
func customTest(th *material.Theme, value float64) *customCell {
s := &customCell{
BasicCell: datatable.BasicCell{
LabelStyle: material.Body1(th, fmt.Sprintf("%.2f", value)),
Value: value,
},
widget: &widget.Clickable{},
}
if value > 70 {
s.icon = material.IconButton(th, s.widget, iconCheck)
s.icon.Background = rgb(0x00ff00)
} else {
s.icon = material.IconButton(th, s.widget, iconUncheck)
s.icon.Background = rgb(0xff0000)
}
s.icon.Inset = layout.UniformInset(unit.Dp(0))
s.icon.Size = unit.Dp(19)
return s
}
type customCell struct {
datatable.BasicCell
icon material.IconButtonStyle
widget *widget.Clickable
}
func (s *customCell) Layout(gtx C) D {
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.End, Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(s.icon.Layout),
layout.Rigid(s.BasicCell.Layout),
)
// return s.icon.Layout(gtx)
}
func loop(w *app.Window) error {
th := material.NewTheme(gofont.Collection())
th.TextSize = unit.Dp(14)
th.Color.Primary = rgb(0x1976D2)
table := datatable.New(th)
table.RowsPerPage = 10
f, err := os.Open("alcohol.csv")
if err != nil {
panic(err)
}
r := csv.NewReader(f)
r.LazyQuotes = true
// r.FieldsPerRecord = -1
headerDone := false
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
if !headerDone {
headerDone = true
for _, title := range record {
table.Header.Columns = append(table.Header.Columns,
table.NewHeader(title, 0.16, text.End),
)
}
} else {
columns := []datatable.Cell{}
id := 0
for _, content := range record {
// v, err := strconv.ParseFloat(strings.TrimSpace(content), 64)
// if err != nil {
// panic(err)
// }
// if i == 0 {
// id, err = strconv.Atoi(content)
// if err != nil {
// panic(err)
// }
// columns = append(columns,
// table.NewCell(v),
// )
// } else if i == 1 {
// columns = append(columns,
// customTest(th, v),
// )
// } else {
// columns = append(columns,
// table.NewCell(v),
// )
// }
columns = append(columns,
table.NewCell(content),
)
}
table.AppendRows(datatable.NewRow(id, columns))
}
}
var ops op.Ops
for {
select {
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
table.Layout(gtx)
e.Frame(gtx.Ops)
}
}
}
}
func main() {
go func() {
w := app.NewWindow(app.Size(unit.Dp(800), unit.Dp(700)))
if err := loop(w); err != nil {
panic(err)
}
os.Exit(0)
}()
app.Main()
}
module prostocklivestock.com.au/thetooth/datatable
go 1.14
require (
gioui.org v0.0.0-20201018162216-7a4b48f67b54
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
)
package datatable
import (
"gioui.org/widget"
"golang.org/x/exp/shiny/materialdesign/icons"
)
var (
iconLeft, iconFLeft *widget.Icon
iconRight, iconFRight *widget.Icon
iconSort *widget.Icon
iconSortAZ, iconSortZA *widget.Icon
)
func init() {
var err error
iconFLeft, err = widget.NewIcon(icons.NavigationFirstPage)
if err != nil {
panic(err)
}
iconLeft, err = widget.NewIcon(icons.NavigationChevronLeft)
if err != nil {
panic(err)
}
iconRight, err = widget.NewIcon(icons.NavigationChevronRight)
if err != nil {
panic(err)
}
iconFRight, err = widget.NewIcon(icons.NavigationLastPage)
if err != nil {
panic(err)
}
iconSort, err = widget.NewIcon(icons.ContentSort)
if err != nil {
panic(err)
}
iconSortAZ, err = widget.NewIcon(icons.NavigationExpandMore)
if err != nil {
panic(err)
}
iconSortZA, err = widget.NewIcon(icons.NavigationExpandLess)
if err != nil {
panic(err)
}
}
package datatable
import (
"gioui.org/widget"
"prostocklivestock.com.au/thetooth/datatable/cui"
)
func NewRow(index int, columns []Cell) Row {
return Row{
Index: index,
Columns: columns,
Selected: &widget.Bool{},
hover: &cui.Hoverable{},
}
}
type Row struct {
Columns []Cell
Index int
Selected *widget.Bool
hover *cui.Hoverable
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment