kanban

Kanban client in Gio
Log | Files | Refs | README | LICENSE

commit f959775d4c2a7924868f8afef3c97b981eda9229
parent ec7ef2212d49e91b5b605170e693b44957dfb4ac
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Fri, 13 Nov 2020 17:42:00 +0800

feat(project-rail): sketch of project rail with hardcoded projects labels

Diffstat:
Mcmd/kanban/main.go | 198++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcmd/kanban/widget.go | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgo.mod | 6+++---
Mgo.sum | 6++++++
4 files changed, 258 insertions(+), 50 deletions(-)

diff --git a/cmd/kanban/main.go b/cmd/kanban/main.go @@ -112,6 +112,7 @@ type UI struct { Kanban *kanban.Kanban Th *material.Theme Panels []Panel + Rail Rail TicketStates Map Modal layout.Widget TicketForm TicketForm @@ -122,6 +123,8 @@ type UI struct { Index int Stage int } + CreateProjectButton widget.Clickable + ProjectForm ProjectForm } func (ui *UI) Loop() error { @@ -182,13 +185,7 @@ func (ui *UI) Update(gtx C) { for ii := range ui.Panels { panel := &ui.Panels[ii] if panel.CreateTicket.Clicked() { - ui.Modal = func(gtx C) D { - return Card{ - Title: "Add Ticket", - }.Layout(gtx, ui.Th, func(gtx C) D { - return ui.TicketForm.Layout(gtx, ui.Th, panel.Label) - }) - } + ui.AddTicket(panel.Label) } } for s, ok := ui.TicketStates.Next(); ok; s, ok = ui.TicketStates.Next() { @@ -253,56 +250,95 @@ func (ui *UI) Update(gtx C) { if ui.TicketDetails.Cancel.Clicked() { ui.Clear() } + if ui.CreateProjectButton.Clicked() { + ui.CreateProject() + } + if ui.ProjectForm.Cancel.Clicked() { + ui.Clear() + } } func (ui *UI) Layout(gtx C) D { key.InputOp{Tag: ui}.Add(gtx.Ops) - return layout.Stack{}.Layout( + return layout.Flex{Axis: layout.Horizontal}.Layout( gtx, - layout.Stacked(func(gtx C) D { - ui.TicketStates.Begin() - var panels = make([]layout.FlexChild, len(ui.Panels)) - for kk := range ui.Panels { - panel := &ui.Panels[kk] - panels[kk] = layout.Flexed(1, func(gtx C) D { - stage, _ := ui.Kanban.Stage(panel.Label) - var cards = make([]layout.ListElement, len(stage.Tickets)) - for ii, ticket := range stage.Tickets { - id := strconv.Itoa(ticket.ID) - cards[ii] = func(gtx C, ii int) D { - t := (*Ticket)(ui.TicketStates.New(id, unsafe.Pointer(&Ticket{}))) - t.Ticket = stage.Tickets[ii] - t.Stage = stage.Name - if ui.FocusedTicket.ID == t.ID { - return widget.Border{ - Color: color.RGBA{B: 200, A: 200}, - Width: unit.Dp(2), - }.Layout(gtx, func(gtx C) D { + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.Y = gtx.Constraints.Max.Y + gtx.Constraints.Max.X = gtx.Px(unit.Dp(80)) + gtx.Constraints.Min.X = 0 + return ui.Rail.Layout( + gtx, + func(gtx C) D { + return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D { + btn := material.IconButton(ui.Th, &ui.CreateProjectButton, icons.ContentAdd) + btn.Size = unit.Dp(20) + btn.Inset = layout.UniformInset(unit.Dp(8)) + return btn.Layout(gtx) + }) + }, + Destination("kanban", func(gtx C) D { + return material.Label(ui.Th, unit.Dp(16), "Kanban").Layout(gtx) + }), + Destination("avisha", func(gtx C) D { + return material.Label(ui.Th, unit.Dp(16), "Avisha").Layout(gtx) + }), + Destination("watch", func(gtx C) D { + return material.Label(ui.Th, unit.Dp(16), "Watch").Layout(gtx) + }), + Destination("gopack", func(gtx C) D { + return material.Label(ui.Th, unit.Dp(16), "GoPack").Layout(gtx) + }), + ) + }), + layout.Flexed(1, func(gtx C) D { + return layout.Stack{}.Layout( + gtx, + layout.Stacked(func(gtx C) D { + ui.TicketStates.Begin() + var panels = make([]layout.FlexChild, len(ui.Panels)) + for kk := range ui.Panels { + panel := &ui.Panels[kk] + panels[kk] = layout.Flexed(1, func(gtx C) D { + stage, _ := ui.Kanban.Stage(panel.Label) + var cards = make([]layout.ListElement, len(stage.Tickets)) + for ii, ticket := range stage.Tickets { + id := strconv.Itoa(ticket.ID) + cards[ii] = func(gtx C, ii int) D { + t := (*Ticket)(ui.TicketStates.New(id, unsafe.Pointer(&Ticket{}))) + t.Ticket = stage.Tickets[ii] + t.Stage = stage.Name + if ui.FocusedTicket.ID == t.ID { + return widget.Border{ + Color: color.RGBA{B: 200, A: 200}, + Width: unit.Dp(2), + }.Layout(gtx, func(gtx C) D { + return t.Layout(gtx, ui.Th) + }) + } return t.Layout(gtx, ui.Th) - }) + } } - return t.Layout(gtx, ui.Th) - } + return panel.Layout(gtx, ui.Th, cards...) + }) } - return panel.Layout(gtx, ui.Th, cards...) - }) - } - return layout.Flex{ - Axis: layout.Horizontal, - Spacing: layout.SpaceEvenly, - }.Layout( - gtx, - panels..., + return layout.Flex{ + Axis: layout.Horizontal, + Spacing: layout.SpaceEvenly, + }.Layout( + gtx, + panels..., + ) + }), + layout.Expanded(func(gtx C) D { + if ui.Modal == nil { + return D{} + } + return Modal(gtx, func(gtx C) D { + return ui.Modal(gtx) + }) + }), ) }), - layout.Expanded(func(gtx C) D { - if ui.Modal == nil { - return D{} - } - return Modal(gtx, func(gtx C) D { - return ui.Modal(gtx) - }) - }), ) } @@ -367,6 +403,7 @@ func (ui *UI) Refocus(d Direction) { func (ui *UI) Clear() { ui.Modal = nil ui.TicketForm = TicketForm{} + ui.ProjectForm = ProjectForm{} ui.FocusedTicket = struct { ID int Index int @@ -398,6 +435,28 @@ func (ui *UI) EditTicket(t kanban.Ticket) { } } +// AddTicket opens the ticket form for creating ticket data. +func (ui *UI) AddTicket(stage string) { + ui.Modal = func(gtx C) D { + return Card{ + Title: "Add Ticket", + }.Layout(gtx, ui.Th, func(gtx C) D { + return ui.TicketForm.Layout(gtx, ui.Th, stage) + }) + } +} + +// CreatTicket opens the project creation dialog. +func (ui *UI) CreateProject() { + ui.Modal = func(gtx C) D { + return Card{ + Title: "Create a new Project", + }.Layout(gtx, ui.Th, func(gtx C) D { + return ui.ProjectForm.Layout(gtx, ui.Th) + }) + } +} + // DeleteTickets opens the confirmation dialog for deleting a ticket. func (ui *UI) DeleteTicket(t kanban.Ticket) { ui.DeleteDialog.Ticket = t @@ -907,3 +966,48 @@ func (t *TicketDetails) Layout(gtx C, th *material.Theme) D { }), ) } + +// ProjectForm renders a form for manipulating projects. +type ProjectForm struct { + Name materials.TextField + Submit widget.Clickable + Cancel widget.Clickable +} + +func (form *ProjectForm) Layout(gtx C, th *material.Theme) D { + return layout.Flex{ + Axis: layout.Vertical, + }.Layout( + gtx, + layout.Rigid(func(gtx C) D { + return form.Name.Layout(gtx, th, "Project Name") + }), + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return layout.Inset{ + Top: unit.Dp(10), + }.Layout(gtx, func(gtx C) D { + return layout.Flex{ + Axis: layout.Horizontal, + }.Layout( + gtx, + layout.Flexed(1, func(gtx C) D { + return D{Size: gtx.Constraints.Min} + }), + layout.Rigid(func(gtx C) D { + btn := material.Button(th, &form.Cancel, "Cancel") + btn.Color = th.Color.Primary + btn.Background = color.RGBA{} + return btn.Layout(gtx) + }), + layout.Rigid(func(gtx C) D { + return D{Size: image.Point{X: gtx.Px(unit.Dp(10))}} + }), + layout.Rigid(func(gtx C) D { + return material.Button(th, &form.Submit, "Submit").Layout(gtx) + }), + ) + }) + }), + ) +} diff --git a/cmd/kanban/widget.go b/cmd/kanban/widget.go @@ -3,6 +3,7 @@ package main import ( "image" "image/color" + "unsafe" "gioui.org/f32" "gioui.org/layout" @@ -104,3 +105,100 @@ func WithInset(inset layout.Inset) ButtonOption { btn.ButtonStyle.Inset = inset } } + +// Rail is an interactive side rail with a list of widget items that can be +// selected. +// +// Typically used as navigation or contextual actions. +// +// Rail is stateful. +type Rail struct { + layout.List + Map Map +} + +// RailChild is an item that renders in a rail. +type RailChild struct { + Name string + W layout.Widget +} + +// Destination is a rail item that represents a navigatable object. +// Destinations are padded by default. +func Destination(name string, w layout.Widget) RailChild { + return RailChild{ + Name: name, + W: w, + } +} + +func (r *Rail) next(key string) *widget.Clickable { + return (*widget.Clickable)(r.Map.New(key, unsafe.Pointer(&widget.Clickable{}))) +} + +// Layout the rail with the given items. +func (r *Rail) Layout(gtx C, action layout.Widget, items ...RailChild) D { + r.List.Axis = layout.Vertical + r.List.Alignment = layout.Middle + r.Map.Begin() + return layout.Flex{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }.Layout( + gtx, + layout.Rigid(func(gtx C) D { + return action(gtx) + }), + layout.Rigid(func(gtx C) D { + return layout.UniformInset(unit.Dp(5)).Layout(gtx, func(gtx C) D { + return Div{ + Color: color.RGBA{R: 220, B: 220, G: 220, A: 255}, + Length: unit.Px(float32(gtx.Constraints.Max.X)), + Thickness: unit.Dp(1), + Axis: layout.Horizontal, + }.Layout(gtx) + }) + }), + layout.Rigid(func(gtx C) D { + return r.List.Layout(gtx, len(items), func(gtx C, ii int) D { + rc := items[ii] + return material.Clickable(gtx, r.next(rc.Name), func(gtx C) D { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx C) D { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return rc.W(gtx) + }) + }) + }) + }), + ) +} + +// Div is a visual divider: a colored line with a thickness. +type Div struct { + Thickness unit.Value + Length unit.Value + Axis layout.Axis + Color color.RGBA +} + +func (d Div) Layout(gtx C) D { + // Draw a line as a very line. + var sz image.Point + switch d.Axis { + case layout.Horizontal: + sz = image.Point{ + X: gtx.Px(d.Length), + Y: gtx.Px(d.Thickness), + } + case layout.Vertical: + sz = image.Point{ + X: gtx.Px(d.Thickness), + Y: gtx.Px(d.Length), + } + } + return Rect{ + Color: d.Color, + Size: layout.FPt(sz), + Radii: 0, + }.Layout(gtx) +} diff --git a/go.mod b/go.mod @@ -3,14 +3,14 @@ module git.sr.ht/~jackmordaunt/kanban go 1.15 require ( - gioui.org v0.0.0-20201110154114-ae256b5be812 - git.sr.ht/~whereswaldon/materials v0.0.0-20201108152851-4af89701ceb6 + gioui.org v0.0.0-20201112145235-3c739323cb4f + git.sr.ht/~whereswaldon/materials v0.0.0-20201111125311-690e8da25ce3 github.com/asdine/storm/v3 v3.2.1 github.com/jackmordaunt/icns v1.0.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pkg/errors v0.9.1 // indirect go.etcd.io/bbolt v1.3.5 // indirect golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7 - golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 // indirect + golang.org/x/sys v0.0.0-20201112073958-5cba982894dd // indirect golang.org/x/text v0.3.4 // indirect ) diff --git a/go.sum b/go.sum @@ -2,8 +2,12 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 gioui.org v0.0.0-20201106195654-dbc0796d0207/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= gioui.org v0.0.0-20201110154114-ae256b5be812 h1:fmETJJuKWcrg5pYKSX/MkwNXv3MBF7LsjBPa4qyO1Cw= gioui.org v0.0.0-20201110154114-ae256b5be812/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= +gioui.org v0.0.0-20201112145235-3c739323cb4f h1:O1WtE27z2GqOJjE0Dl6k2qqts63tqoSwwoWyE7K3RLM= +gioui.org v0.0.0-20201112145235-3c739323cb4f/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= git.sr.ht/~whereswaldon/materials v0.0.0-20201108152851-4af89701ceb6 h1:R/9jzikSdVeHbeJx5WU3e4lNRHzxLt2HduMWLa78m5k= git.sr.ht/~whereswaldon/materials v0.0.0-20201108152851-4af89701ceb6/go.mod h1:TayKd2ixEIPSq7ZzJIb1hcIsV1FapB43GCSx8GnLyUg= +git.sr.ht/~whereswaldon/materials v0.0.0-20201111125311-690e8da25ce3 h1:885LotAnlGb2X4ZBmaqbkmVpW+KkQJEALwmHcm4WHo8= +git.sr.ht/~whereswaldon/materials v0.0.0-20201111125311-690e8da25ce3/go.mod h1:C1KEwj50dyQ+TpIaomIgBqhnSbbB34UqeFvuCOuddro= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= @@ -75,6 +79,8 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfru golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 h1:Qo9oJ566/Sq7N4hrGftVXs8GI2CXBCuOd4S2wHE/e0M= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=