commit 5f4e99bf4057f718a186e85aa9a0a01674b19c69
parent 9b03d878573d85bec88905adda4375d2077b380f
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Sat, 27 Feb 2021 19:30:54 +0800
ref: use material card as layout primitive
Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
Diffstat:
3 files changed, 212 insertions(+), 235 deletions(-)
diff --git a/cmd/kanban/control/card.go b/cmd/kanban/control/card.go
@@ -1,21 +1,34 @@
package control
import (
+ "image"
"image/color"
- "gioui.org/f32"
"gioui.org/layout"
"gioui.org/unit"
+ "gioui.org/widget"
"gioui.org/widget/material"
"git.sr.ht/~jackmordaunt/kanban/cmd/kanban/util"
)
-// Card lays the content out with a title for context.
+// Card implements "https://material.io/components/cards".
type Card struct {
- Title string
+ // Media image.Image
+ Title string
+ Subtitle string
+ Body layout.Widget
+ Actions []Action
}
-func (c Card) Layout(gtx C, th *material.Theme, w layout.Widget) D {
+type Action struct {
+ *widget.Clickable
+ Label string
+ Fg color.NRGBA
+ Bg color.NRGBA
+}
+
+func (c Card) Layout(gtx C, th *material.Theme) D {
+ // @cleanup: spacing strategy is adhoc.
return layout.Stack{}.Layout(
gtx,
layout.Expanded(func(gtx C) D {
@@ -26,36 +39,75 @@ func (c Card) Layout(gtx C, th *material.Theme, w layout.Widget) D {
}.Layout(gtx)
}),
layout.Stacked(func(gtx C) D {
- inset := layout.UniformInset(unit.Dp(10))
- return layout.Flex{
- Axis: layout.Vertical,
- }.Layout(
- gtx,
- layout.Rigid(func(gtx C) D {
- return layout.Stack{}.Layout(
- gtx,
- layout.Expanded(func(gtx C) D {
- return util.Rect{
- Color: color.NRGBA{A: 100},
- Size: f32.Point{
- X: float32(gtx.Constraints.Max.X),
- Y: float32(gtx.Constraints.Min.Y),
- },
- }.Layout(gtx)
- }),
- layout.Stacked(func(gtx C) D {
- return inset.Layout(gtx, func(gtx C) D {
- return material.H6(th, c.Title).Layout(gtx)
- })
- }),
- )
- }),
- layout.Rigid(func(gtx C) D {
- return inset.Layout(gtx, func(gtx C) D {
- return w(gtx)
- })
- }),
- )
+ return layout.Inset{
+ Bottom: unit.Dp(20),
+ Left: unit.Dp(15),
+ Right: unit.Dp(15),
+ }.Layout(gtx, func(gtx C) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(
+ gtx,
+ layout.Rigid(func(gtx C) D {
+ return layout.Inset{
+ Top: unit.Dp(20),
+ Bottom: unit.Dp(20),
+ }.Layout(gtx, func(gtx C) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(
+ gtx,
+ layout.Rigid(func(gtx C) D {
+ return material.H5(th, c.Title).Layout(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ if c.Subtitle == "" {
+ return D{}
+ }
+ return D{Size: image.Point{Y: gtx.Px(unit.Dp(10))}}
+ }),
+ layout.Rigid(func(gtx C) D {
+ if c.Subtitle == "" {
+ return D{}
+ }
+ return material.Body1(th, c.Subtitle).Layout(gtx)
+ }),
+ )
+ })
+ }),
+ layout.Rigid(func(gtx C) D {
+ if c.Body == nil {
+ return D{}
+ }
+ return c.Body(gtx)
+ }),
+ layout.Rigid(func(gtx C) D {
+ return D{Size: image.Point{Y: gtx.Px(unit.Dp(20))}}
+ }),
+ layout.Rigid(func(gtx C) D {
+ if len(c.Actions) < 1 {
+ return D{}
+ }
+ return layout.Flex{
+ Axis: layout.Horizontal,
+ }.Layout(
+ gtx,
+ func() (actions []layout.FlexChild) {
+ for ii := range c.Actions {
+ action := &c.Actions[ii]
+ actions = append(actions, layout.Rigid(func(gtx C) D {
+ btn := material.Button(th, action.Clickable, action.Label)
+ btn.Color = action.Fg
+ btn.Background = action.Bg
+ return btn.Layout(gtx)
+ }))
+ }
+ return actions
+ }()...,
+ )
+ }),
+ )
+ })
}),
)
}
diff --git a/cmd/kanban/ui.go b/cmd/kanban/ui.go
@@ -457,11 +457,7 @@ func (ui *UI) Clear() {
func (ui *UI) InspectTicket(t kanban.Ticket) {
ui.TicketDetails.Ticket = t
ui.Modal = func(gtx C) D {
- return control.Card{
- Title: t.Title,
- }.Layout(gtx, ui.Th, func(gtx C) D {
- return ui.TicketDetails.Layout(gtx, ui.Th)
- })
+ return ui.TicketDetails.Layout(gtx, ui.Th)
}
}
@@ -469,11 +465,7 @@ func (ui *UI) InspectTicket(t kanban.Ticket) {
func (ui *UI) EditTicket(t kanban.Ticket) {
ui.TicketForm.Set(t)
ui.Modal = func(gtx C) D {
- return control.Card{
- Title: "Edit Ticket",
- }.Layout(gtx, ui.Th, func(gtx C) D {
- return ui.TicketForm.Layout(gtx, ui.Th, "")
- })
+ return ui.TicketForm.Layout(gtx, ui.Th, "")
}
}
@@ -481,11 +473,7 @@ func (ui *UI) EditTicket(t kanban.Ticket) {
func (ui *UI) AddTicket(stage string) {
ui.TicketForm.Title.Focus()
ui.Modal = func(gtx C) D {
- return control.Card{
- Title: "Add Ticket",
- }.Layout(gtx, ui.Th, func(gtx C) D {
- return ui.TicketForm.Layout(gtx, ui.Th, stage)
- })
+ return ui.TicketForm.Layout(gtx, ui.Th, stage)
}
}
@@ -493,11 +481,7 @@ func (ui *UI) AddTicket(stage string) {
func (ui *UI) CreateProject() {
ui.ProjectForm.Name.Focus()
ui.Modal = func(gtx C) D {
- return control.Card{
- Title: "Create a new Project",
- }.Layout(gtx, ui.Th, func(gtx C) D {
- return ui.ProjectForm.Layout(gtx, ui.Th)
- })
+ return ui.ProjectForm.Layout(gtx, ui.Th)
}
}
@@ -505,11 +489,7 @@ func (ui *UI) CreateProject() {
func (ui *UI) DeleteTicket(t kanban.Ticket) {
ui.DeleteDialog.Ticket = t
ui.Modal = func(gtx C) D {
- return control.Card{
- Title: "Delete Ticket",
- }.Layout(gtx, ui.Th, func(gtx C) D {
- return ui.DeleteDialog.Layout(gtx, ui.Th)
- })
+ return ui.DeleteDialog.Layout(gtx, ui.Th)
}
}
diff --git a/cmd/kanban/widgets.go b/cmd/kanban/widgets.go
@@ -14,8 +14,10 @@ import (
"gioui.org/widget/material"
"gioui.org/x/component"
"git.sr.ht/~jackmordaunt/kanban"
+ "git.sr.ht/~jackmordaunt/kanban/cmd/kanban/control"
"git.sr.ht/~jackmordaunt/kanban/cmd/kanban/util"
"git.sr.ht/~jackmordaunt/kanban/icons"
+ "github.com/google/uuid"
)
// TicketForm renders the form for ticket information.
@@ -54,47 +56,44 @@ func (f TicketForm) Submit() kanban.Ticket {
func (form *TicketForm) Layout(gtx C, th *material.Theme, stage string) D {
form.Stage = stage
form.Title.SingleLine = true
- return layout.Flex{
- Axis: layout.Vertical,
- }.Layout(
- gtx,
- layout.Rigid(func(gtx C) D {
- return form.Title.Layout(gtx, th, "Title")
- }),
- layout.Rigid(func(gtx C) D {
- return form.Summary.Layout(gtx, th, "Summary")
- }),
- layout.Rigid(func(gtx C) D {
- return form.Details.Layout(gtx, th, "Details")
- }),
- 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.CancelBtn, "Cancel")
- btn.Color = th.Fg
- btn.Background = color.NRGBA{}
- 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.SubmitBtn, "Submit").Layout(gtx)
- }),
- )
- })
- }),
- )
+ return control.Card{
+ Title: func() string {
+ if form.Ticket.ID == uuid.Nil {
+ return "Add Ticket"
+ }
+ return "Edit Ticket"
+ }(),
+ Body: func(gtx C) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(
+ gtx,
+ layout.Rigid(func(gtx C) D {
+ return form.Title.Layout(gtx, th, "Title")
+ }),
+ layout.Rigid(func(gtx C) D {
+ return form.Summary.Layout(gtx, th, "Summary")
+ }),
+ layout.Rigid(func(gtx C) D {
+ return form.Details.Layout(gtx, th, "Details")
+ }),
+ )
+ },
+ Actions: []control.Action{
+ {
+ Clickable: &form.SubmitBtn,
+ Label: "Submit",
+ Fg: th.ContrastFg,
+ Bg: th.ContrastBg,
+ },
+ {
+ Clickable: &form.CancelBtn,
+ Label: "Cancel",
+ Fg: th.Fg,
+ Bg: th.Bg,
+ },
+ },
+ }.Layout(gtx, th)
}
// ProjectForm renders a form for manipulating projects.
@@ -105,41 +104,33 @@ type ProjectForm struct {
}
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.Fg
- btn.Background = color.NRGBA{}
- 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)
- }),
- )
- })
- }),
- )
+ return control.Card{
+ Title: "Create a new Project",
+ Body: func(gtx C) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ }.Layout(
+ gtx,
+ layout.Rigid(func(gtx C) D {
+ return form.Name.Layout(gtx, th, "Project Name")
+ }),
+ )
+ },
+ Actions: []control.Action{
+ {
+ Clickable: &form.Submit,
+ Label: "Submit",
+ Fg: th.ContrastFg,
+ Bg: th.ContrastBg,
+ },
+ {
+ Clickable: &form.Cancel,
+ Label: "Cancel",
+ Fg: th.Fg,
+ Bg: th.Bg,
+ },
+ },
+ }.Layout(gtx, th)
}
// DeleteDialog prompts the user with an option to delete a ticket.
@@ -150,49 +141,37 @@ type DeleteDialog struct {
}
func (d *DeleteDialog) Layout(gtx C, th *material.Theme) D {
- return layout.Flex{
- Axis: layout.Vertical,
- Alignment: layout.Middle,
- }.Layout(
- gtx,
- layout.Rigid(func(gtx C) D {
- return layout.Center.Layout(gtx, func(gtx C) D {
- return material.Body1(
- th,
- fmt.Sprintf("Are you sure you want to delete ticket %q?", d.Title),
- ).Layout(gtx)
- })
- }),
- 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, &d.Cancel, "Cancel")
- btn.Color = th.Fg
- btn.Background = color.NRGBA{}
- 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 {
- btn := material.Button(th, &d.Ok, "Delete")
- btn.Background = color.NRGBA{R: 200, A: 255}
- return btn.Layout(gtx)
- }),
- )
- })
- }),
- )
+ return control.Card{
+ Title: "Are you sure?",
+ Body: func(gtx C) D {
+ return layout.Flex{
+ Axis: layout.Vertical,
+ Alignment: layout.Middle,
+ }.Layout(
+ gtx,
+ layout.Rigid(func(gtx C) D {
+ return material.Body1(
+ th,
+ fmt.Sprintf("Delete ticket %q?", d.Title),
+ ).Layout(gtx)
+ }),
+ )
+ },
+ Actions: []control.Action{
+ {
+ Clickable: &d.Ok,
+ Label: "Delete",
+ Fg: th.ContrastFg,
+ Bg: color.NRGBA{R: 200, A: 255},
+ },
+ {
+ Clickable: &d.Cancel,
+ Label: "Cancel",
+ Fg: th.Fg,
+ Bg: th.Bg,
+ },
+ },
+ }.Layout(gtx, th)
}
// Ticket renders a ticket control.
@@ -270,8 +249,6 @@ func (t *Ticket) Layout(gtx C, th *material.Theme, focused bool) D {
})
}
-// @todo: de-emphasize details content.
-// @todo: summarize based on card size rather than content length.
func (t *Ticket) content(gtx C, th *material.Theme) D {
macro := op.Record(gtx.Ops)
dims := layout.Inset{
@@ -289,12 +266,9 @@ func (t *Ticket) content(gtx C, th *material.Theme) D {
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: unit.Dp(10)}.Layout(gtx, func(gtx C) D {
- return material.Body1(th, t.Summary).Layout(gtx)
- })
- }),
- layout.Rigid(func(gtx C) D {
- return layout.Inset{Top: unit.Dp(10)}.Layout(gtx, func(gtx C) D {
- return material.Body1(th, summarize(t.Details, 0)).Layout(gtx)
+ l := material.Body1(th, t.Summary)
+ l.Color = component.WithAlpha(l.Color, 200)
+ return l.Layout(gtx)
})
}),
)
@@ -429,54 +403,25 @@ type TicketDetails struct {
}
func (t *TicketDetails) Layout(gtx C, th *material.Theme) D {
- return layout.Flex{
- Axis: layout.Vertical,
- }.Layout(
- gtx,
- layout.Rigid(func(gtx C) D {
- return material.Body1(th, t.Summary).Layout(gtx)
- }),
- layout.Rigid(func(gtx C) D {
+ return control.Card{
+ Title: t.Title,
+ Subtitle: t.Summary,
+ Body: func(gtx C) D {
return material.Body1(th, t.Details).Layout(gtx)
- }),
- 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, &t.Cancel, "Cancel")
- btn.Color = th.Fg
- btn.Background = color.NRGBA{}
- 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, &t.Edit, "Edit").Layout(gtx)
- }),
- )
- })
- }),
- )
-}
-
-// summarize truncates string s after n amount of characters, and appends an
-// elipsis to indicate missing content.
-func summarize(s string, n int) string {
- if n == 0 {
- n = 70
- }
- if len(s) < n {
- return s
- }
- return strings.TrimSpace(s[:n]) + string('\u2026')
+ },
+ Actions: []control.Action{
+ {
+ Clickable: &t.Edit,
+ Label: "Edit",
+ Fg: th.ContrastFg,
+ Bg: th.ContrastBg,
+ },
+ {
+ Clickable: &t.Cancel,
+ Label: "Cancel",
+ Fg: th.Fg,
+ Bg: th.Bg,
+ },
+ },
+ }.Layout(gtx, th)
}