-
-
Save thetooth/c04ea3d572ed554fb87d98e830e11921 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}} | |
}), | |
) | |
}) | |
}), | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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