From 3724f62497b9ec414a7af3e6c23807388d6cf229 Mon Sep 17 00:00:00 2001 From: Greg Date: Thu, 22 Aug 2019 10:09:55 -0400 Subject: [PATCH] Add Grid layout element and calendar example (cmd/cal). --- .gitignore | 1 + cmd/cal/datetime.go | 259 ++++++++++++++++++++++++++++++++++++++++++++ cmd/cal/main.go | 193 +++++++++++++++++++++++++++++++++ main.go | 108 +++++++++++++++++- 4 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 cmd/cal/datetime.go create mode 100644 cmd/cal/main.go diff --git a/.gitignore b/.gitignore index 3ebee80..05de7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ cmd/hello/hello +cmd/cal/cal diff --git a/cmd/cal/datetime.go b/cmd/cal/datetime.go new file mode 100644 index 0000000..42c1b76 --- /dev/null +++ b/cmd/cal/datetime.go @@ -0,0 +1,259 @@ +package main + +import ( + "time" +) + +func _daysIn(t time.Time) int { + y,m,_ := t.Date() + t2 := time.Date(y,m+1,1,0,0,0,0,time.UTC) + return t2.AddDate(0,0,-1).Day() +} + +type SelectableMonth struct { + Ws [7]bool + S [32]bool + Year, Pad, DaysIn int + Month time.Month + Weekday time.Weekday +} + +func NewMonth(t time.Time) *SelectableMonth { + t = time.Date(t.Year(), t.Month(),1,0,0,0,0,time.Local) + ret := &SelectableMonth{} + ret.Year, ret.Month, ret.Weekday = t.Year(), t.Month(), t.Weekday() + ret.DaysIn = _daysIn(t) + ret.Pad = int(ret.Weekday) + return ret +} + +func _newMonth(y int, m time.Month) SelectableMonth { + return *NewMonth(time.Date(y,m,1,0,0,0,0,time.Local)) +} + +func (sm *SelectableMonth) Previous() { + m, y := sm.Month - 1, sm.Year + if m > 11 { + m = 0 + y += 1 + } + *sm = _newMonth(y,m) +} + +func (sm *SelectableMonth) Next() { + m, y := sm.Month + 1, sm.Year + if m <0 { + m = 11 + y -= 1 + } + *sm = _newMonth(y,m) +} + +func (sm *SelectableMonth) SelectWeekday(d time.Weekday) *SelectableMonth { + for i := int(d) + 1 - sm.Pad; i <= 31; i += 7 { + if i < 0 { + continue + } + sm.S[i] = true + } + sm.Ws[d] = true + return sm +} + +func (sm *SelectableMonth) DeselectWeekday(d time.Weekday) *SelectableMonth { + for i := int(d) + 1 - sm.Pad; i <= 31; i += 7 { + if i < 0 { + continue + } + sm.S[i] = false + } + sm.Ws[d] = false + return sm +} + +func (sm *SelectableMonth) CheckSel() *SelectableMonth { + alldays := func(d time.Weekday) bool { + for i := int(d) + 1 - sm.Pad; i <= 31; i+= 7 { + if i < 1 { + continue + } + if !sm.S[i] { + return false + } + } + return true + } + + for d := time.Sunday; d < 7; d++ { + if alldays(d) { + sm.Ws[d] = true + } else { + sm.Ws[d] = false + } + } + return sm +} + +func (sm *SelectableMonth) Weekdays() int { + var i int + for w := 0; w <= 6; w++ { + for d := 1; d < 6; d++ { + x := w * 7 + d + 1 - sm.Pad + if x < 0 || x >= sm.DaysIn { break } + if sm.S[x] { + i++ + } + } + } + return i +} + +func (sm *SelectableMonth) Weekends() int { + var i int + for w := 0; w <= 6; w++ { + x := w*7 + 1 - sm.Pad + if x < 0 { continue } + if x >= sm.DaysIn { break } + if sm.S[x] { i++ } + x = w*7+6 + 1 - sm.Pad + if x < 0 || x >= sm.DaysIn { break } + if sm.S[x] { i++ } + } + return i +} + +// a day within a SelectableMonth +type Day struct { + Num int + sm *SelectableMonth +} + +func (sm *SelectableMonth) Day(i int) *Day { + ret := &Day{Num: i, sm: sm} + return ret.ifexists() +} + +func (d *Day) exists() bool { + if d == nil { + return false + } + if d.Num > d.sm.DaysIn || d.Num <= 0 { + return false + } else { + return true + } +} + +func (d *Day) ifexists() *Day { + if d.exists() { + return d + } else { + return nil + } +} + +func (d *Day) Next() *Day { + ret := &Day{d.Num+1, d.sm} + return ret.ifexists() +} + +func (d *Day) Prev() *Day { + ret := &Day{d.Num-1, d.sm} + return ret.ifexists() +} + +func (sm *SelectableMonth) Days() []*Day { + ret := make([]*Day,0) + for d := sm.Day(1); d.exists(); d = d.Next() { + ret = append(ret,d) + } + return ret +} + +func (d *Day) Weekday() time.Weekday { + return time.Weekday((d.Num + d.sm.Pad - 1) % 7) +} + +func (d *Day) Selected() bool { + return d.sm.S[d.Num] +} + +// a week within a SelectableMonth + +type Week struct { + Num int + sm *SelectableMonth +} + +func (w *Week) exists() bool { + if w == nil { + return false + } + return w.Day(1).exists() // first day of this week exists +} + +func (w *Week) ifexists() *Week { + if w.exists() { + return w + } else { + return nil + } +} + +// week of a month, starting with Sundays except for the first week +func (sm *SelectableMonth) Week(i int) *Week { + ret := &Week{i, sm} + return ret.ifexists() +} + +func (sm *SelectableMonth) Weeks() []*Week { + ret := make([]*Week,0) + for w := sm.Week(1); w.exists(); w = w.Next() { + ret = append(ret,w) + } + return ret +} + +func (w *Week) Day(i int) *Day { + if i < 1 || i > 7 { + return nil + } + wday := (w.Num - 1) * 7 + i + if w.Num != 1 { + wday -= w.sm.Pad + } + ret := &Day{ wday, w.sm } + return ret.ifexists() +} + +func (w *Week) Next() *Week { + ret := &Week{ w.Num + 1, w.sm } + return ret.ifexists() +} + +func (w *Week) Prev() *Week { + ret := &Week{ w.Num - 1, w.sm } + return ret.ifexists() +} + +func (w *Week) Days() []*Day { + ret := make([]*Day,0) + for d := w.Day(1); d.In(w); d = d.Next() { + ret = append(ret,d) + } + return ret +} + +func (d *Day) In(w *Week) bool { + if d == nil { + return false + } + fd := w.Day(1) + if d.Num < fd.Num { + return false + } + if d.Num > fd.Num && d.Weekday() == time.Sunday { + return false + } + return true +} diff --git a/cmd/cal/main.go b/cmd/cal/main.go new file mode 100644 index 0000000..f519d6b --- /dev/null +++ b/cmd/cal/main.go @@ -0,0 +1,193 @@ +// +build darwin linux + +package main + +import ( + "fmt" + "log" + "image/color" + "time" + + gio "git.wow.st/gmp/giowrap" + + "gioui.org/ui" + "gioui.org/ui/app" + "gioui.org/ui/layout" + //"gioui.org/ui/measure" + "gioui.org/ui/text" + + "golang.org/x/image/font/sfnt" + "golang.org/x/image/font/gofont/goregular" +) + +var ( + sel int32 + face text.Face +) + +func main() { + log.Print("Staring event loop") + go eventloop() + app.Main() + log.Print("App closed") +} + +func NewButton(t string, lops ...gio.LabelOption) gio.Clickable { + lops = append([]gio.LabelOption{gio.Face(face), gio.Align(text.Center)}, lops...) + lbl := gio.NewLabel(t, lops...) + bg := gio.NewBackground( + gio.Color(color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xe0}), + gio.Radius(ui.Dp(4))) + return gio.AsClickable(bg(lbl)) +} + +type SLabel struct { + w gio.Clickable + bg *gio.Background + active *bool +} + +func NewSLabel(t string, active *bool, lops ...gio.LabelOption) *SLabel { + ret := &SLabel{} + lops = append([]gio.LabelOption{gio.Face(face)},lops...) + lbl := gio.NewLabel(t, lops...) + bg := &gio.Background{ + Color: color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xe0}, + Radius: ui.Dp(4), + Inset: layout.UniformInset(ui.Dp(4)), + } + if *active { + bg.Color = color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xe0} + } + l := gio.AsClickable(gio.Enclose(bg,lbl)) + ret.w = l + ret.bg = bg + ret.active = active + return ret +} + +func (w *SLabel) Layout(ctx *gio.Context) { + if *w.active { + w.bg.Color = color.RGBA{A: 0xff, R: 0xe0, G: 0xe0, B: 0xe0} + } else { + w.bg.Color = color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xe0} + } + w.w.Layout(ctx) + if w.w.Clicked(ctx) { + if *w.active { + *w.active = false + } else { + *w.active = true + } + } +} + +func Rows(n int) gio.WidgetCombinator { + f1 := gio.NewFlex(gio.Axis(layout.Vertical)) + f2 := gio.NewFlex(gio.Axis(layout.Horizontal), + gio.MainAxisAlignment(layout.SpaceAround)) + + return func(ws ...gio.Widget) gio.Widget { + cs := make([]gio.Widget, (len(ws)+n-1) / n) + for c := 0; c < len(cs); c++ { + end := (c + 1) * n + if end > len(ws) { + end = len(ws) + } + cs[c] = f2(ws[c*n:end]...) + } + return f1(cs...) + } +} + +func NewCal(sm *SelectableMonth) gio.Widget { + pad := gio.NewLabel("", gio.Face(face), gio.Align(text.End)) + ds := make([]gio.Widget,42) + for i := 0; i < len(ds); i++ { + if i < sm.Pad || i >= sm.DaysIn + sm.Pad { + ds[i] = pad + } else { + ds[i] = NewSLabel(fmt.Sprintf("%d",i+1-sm.Pad), &sm.S[i+1-sm.Pad], gio.Align(text.End)) + } + } + return gio.NewGrid(7)(ds...) +} + +func eventloop() { + w := app.NewWindow(&app.WindowOptions{ + Width: ui.Dp(600), Height: ui.Dp(600), Title: "Tickets"}) + ctx := gio.NewContext(w) + sm := NewMonth(time.Now()) + + regular, err := sfnt.Parse(goregular.TTF) + face = ctx.Faces.For(regular, ui.Sp(20)) + if err != nil { log.Fatal("Cannot parse font.") } + + margin := gio.NewInset(gio.Size(ui.Dp(10))) + topgrid := gio.NewGrid(3) + lbtn := NewButton(" < ") + mth := gio.NewLabel("", gio.Face(face), gio.Align(text.Center)) + rbtn := NewButton(" > ") + f1 := gio.NewFlex(gio.Axis(layout.Vertical)) + + bg := gio.NewBackground( + gio.Color(color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xe0})) + + var cal gio.Widget + + daygrid := gio.NewGrid(7) + sun := NewSLabel("Sun",&sm.Ws[time.Sunday], gio.Align(text.End)) + mon := NewSLabel("Mon",&sm.Ws[time.Monday], gio.Align(text.End)) + tue := NewSLabel("Tue",&sm.Ws[time.Tuesday], gio.Align(text.End)) + wed := NewSLabel("Wed",&sm.Ws[time.Wednesday], gio.Align(text.End)) + thu := NewSLabel("Thu",&sm.Ws[time.Thursday], gio.Align(text.End)) + fri := NewSLabel("Fri",&sm.Ws[time.Friday], gio.Align(text.End)) + sat := NewSLabel("Sat",&sm.Ws[time.Saturday], gio.Align(text.End)) + + resetCal := func() { + cal = NewCal(sm) + w.Invalidate() + } + resetCal() + + for { select { + case e:= <-w.Events(): + switch e := e.(type) { + case app.DestroyEvent: + return + case app.DrawEvent: + ctx.Reset(e) + + mth.SetText(fmt.Sprintf("%s %d",sm.Month.String(),sm.Year)) + + ows := sm.Ws + bg(margin(f1( + topgrid(lbtn,mth,rbtn), + daygrid(sun,mon,tue,wed,thu,fri,sat), + cal, + ))).Layout(ctx) + ctx.Draw() + + for i := time.Sunday; i < 7; i++ { + switch { + case sm.Ws[i] && !ows[i]: + sm.SelectWeekday(i) + resetCal() + case !sm.Ws[i] && ows[i]: + sm.DeselectWeekday(i) + resetCal() + } + } + if lbtn.Clicked(ctx) { + sm.Previous() + resetCal() + } + if rbtn.Clicked(ctx) { + sm.Next() + resetCal() + } + sm.CheckSel() + } + }} +} + diff --git a/main.go b/main.go index fedb545..5fb6359 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package giowrap import ( + "image" "image/color" //"log" "sync" @@ -348,7 +349,8 @@ func (bg *Background) Begin(ctx *Context) { func (bg *Background) End(ctx *Context) { ctx.dims = bg.Inset.End(ctx.dims) bg.macro.Stop() - //var stack ui.StackOp + var stack ui.StackOp + stack.Push(ctx.ops) w, h := float32(ctx.dims.Size.X), float32(ctx.dims.Size.Y) if r := float32(ctx.c.Px(bg.Radius)); r > 0 { if r > w / 2 { @@ -362,6 +364,7 @@ func (bg *Background) End(ctx *Context) { gdraw.ColorOp{Color: bg.Color}.Add(ctx.ops) gdraw.DrawOp{Rect: f32.Rectangle{Max: f32.Point{X: w, Y: h}}}.Add(ctx.ops) bg.macro.Add(ctx.ops) + stack.Pop() } // https://pomax.github.io/bezierinfo/#circles_cubic. @@ -412,3 +415,106 @@ func (w cWidget) Clicked(ctx *Context) bool { func AsClickable(w Widget) cWidget { return cWidget{ w: w, click: new(gesture.Click) } } + +type Grid struct { + Axis layout.Axis + Cols int + Height int + + macro ui.MacroOp + ops *ui.Ops + cs layout.Constraints + mode gridMode + row, col int +} + +type GridChild struct { + dims layout.Dimens + macro ui.MacroOp +} + +type gridMode uint8 +const ( + modeNone gridMode = iota + modeBegun +) + +func (g *Grid) Init(ops *ui.Ops, cs layout.Constraints) layout.Constraints { + g.mode = modeBegun + g.ops = ops + g.cs = cs + g.row, g.col = 0, 0 + + cs.Height.Min = 0 + if g.Height != 0 { + cs.Height.Max = g.Height + } + cs.Width.Max = g.cs.Width.Max / g.Cols + cs.Width.Min = cs.Width.Max + return cs +} + +func (g *Grid) Begin() { + g.macro.Record(g.ops) +} + +func (g *Grid) End(dims layout.Dimens) GridChild { + if g.mode != modeBegun { + panic("Must call Grid.Begin() before adding children.") + } + g.macro.Stop() + return GridChild{ dims: dims, macro: g.macro } +} + +func (g *Grid) Layout(cs ...GridChild) layout.Dimens { + rowheight := 0 + height := 0 + var width float32 + for _,c := range cs { + var stack ui.StackOp + stack.Push(g.ops) + c.macro.Add(g.ops) + stack.Pop() + if c.dims.Size.Y > rowheight { + rowheight = c.dims.Size.Y + } + g.col = g.col+1 + x := float32(g.cs.Width.Max / g.Cols) + ui.TransformOp{}.Offset(f32.Point{X: x}).Add(g.ops) + width = width + x + if g.col >= g.Cols { + g.col = 0 + ui.TransformOp{}.Offset(f32.Point{ X: -width, Y: float32(rowheight) }).Add(g.ops) + g.row = g.row + 1 + height = height + rowheight + width = 0 + rowheight = 0 + } + } + if height == 0 { height = rowheight } + g.mode = modeNone + return layout.Dimens{ Size: image.Point{ g.cs.Width.Max, height } } +} + +func toPointF(p image.Point) f32.Point { + return f32.Point{X: float32(p.X), Y: float32(p.Y)} +} + +func NewGrid(cols int) WidgetCombinator { + g := &Grid{ Cols: cols } + gcs := make([]GridChild,0) + return func(ws ...Widget) Widget { + return NewfWidget(func(ctx *Context) { + cs := g.Init(ctx.ops, ctx.cs) + ctx.cs = cs + for _,w := range ws { + g.Begin() + w.Layout(ctx) + ctx.cs = cs + gcs = append(gcs,g.End(ctx.dims)) + } + ctx.dims = g.Layout(gcs...) + gcs = gcs[0:0] + }) + } +}