theme/material: add the material theme

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-10-11 11:50:51 +02:00
parent ff3fc7a24a
commit abb99eca5c
9 changed files with 537 additions and 15 deletions
+2 -1
View File
@@ -3,6 +3,7 @@ module gioui.org
go 1.13
require (
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a
)
+21 -2
View File
@@ -1,6 +1,25 @@
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/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/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Unlicense OR MIT
package widget
import (
"time"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/layout"
"gioui.org/op"
)
type Button struct {
click gesture.Click
clicks int
history []Click
}
// Click represents a historic click.
type Click struct {
Position f32.Point
Time time.Time
}
func (b *Button) Clicked(gtx *layout.Context) bool {
for _, e := range b.click.Events(gtx) {
switch e.Type {
case gesture.TypeClick:
b.clicks++
case gesture.TypePress:
b.history = append(b.history, Click{
Position: e.Position,
Time: gtx.Now(),
})
}
}
if b.clicks > 0 {
b.clicks--
if b.clicks > 0 {
// Ensure timely delivery of remaining clicks.
op.InvalidateOp{}.Add(gtx.Ops)
}
return true
}
return false
}
func (b *Button) Active() bool {
return b.click.Active()
}
func (b *Button) History() []Click {
return b.history
}
func (b *Button) Layout(gtx *layout.Context) {
b.click.Add(gtx.Ops)
if !b.Active() {
b.clicks = 0
}
for len(b.history) > 0 {
c := b.history[0]
if gtx.Now().Sub(c.Time) < 1*time.Second {
break
}
copy(b.history, b.history[1:])
b.history = b.history[:len(b.history)-1]
}
}
+6
View File
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package widget implements common user interface controls. Widgets
// contain peristent state and process user events. Theme packages
// such as `widget/material` implements drawing of widgets.
package widget
+209
View File
@@ -0,0 +1,209 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package material implements the Material design.
package material
import (
"image"
"image/color"
"image/draw"
"gioui.org/f32"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"golang.org/x/exp/shiny/iconvg"
)
type Button struct {
Text string
// Color is the text color.
Color color.RGBA
Font text.Font
Background color.RGBA
shaper *text.Shaper
}
type IconButton struct {
Background color.RGBA
Icon *Icon
Size unit.Value
Padding unit.Value
}
type Icon struct {
src []byte
size unit.Value
// Cached values.
img image.Image
imgSize int
}
func (t *Theme) Button(txt string) Button {
return Button{
Text: txt,
Color: rgb(0xffffff),
Background: t.Color.Primary,
Font: text.Font{
Size: t.TextSize.Scale(14.0 / 16.0),
},
shaper: t.Shaper,
}
}
// NewIcon returns a new Icon from IconVG data.
func NewIcon(data []byte) (*Icon, error) {
_, err := iconvg.DecodeMetadata(data)
if err != nil {
return nil, err
}
return &Icon{src: data}, nil
}
func (t *Theme) IconButton(icon *Icon) IconButton {
return IconButton{
Background: t.Color.Primary,
Icon: icon,
Size: unit.Dp(56),
Padding: unit.Dp(20),
}
}
func (b Button) Layout(gtx *layout.Context, button *widget.Button) {
col := b.Color
bgcol := b.Background
if !button.Active() {
col.A = 0xaa
bgcol.A = 0xaa
}
st := layout.Stack{}
lbl := st.Rigid(gtx, func() {
layout.UniformInset(unit.Dp(16)).Layout(gtx, func() {
paint.ColorOp{Color: col}.Add(gtx.Ops)
widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.Text)
})
pointer.RectAreaOp{Rect: image.Rectangle{Max: gtx.Dimensions.Size}}.Add(gtx.Ops)
button.Layout(gtx)
})
bg := st.Expand(gtx, func() {
rr := float32(gtx.Px(unit.Dp(4)))
rrect(gtx.Ops,
float32(gtx.Constraints.Width.Max),
float32(gtx.Constraints.Height.Max),
rr, rr, rr, rr,
)
fill(gtx, bgcol)
for _, c := range button.History() {
drawInk(gtx, c)
}
})
st.Layout(gtx, bg, lbl)
}
func (b IconButton) Layout(gtx *layout.Context, button *widget.Button) {
st := layout.Stack{}
ico := st.Rigid(gtx, func() {
layout.UniformInset(b.Padding).Layout(gtx, func() {
size := gtx.Px(b.Size) - gtx.Px(b.Padding)
ico := b.Icon.image(size)
paint.ImageOp{Src: ico, Rect: ico.Bounds()}.Add(gtx.Ops)
paint.PaintOp{
Rect: toRectF(ico.Bounds()),
}.Add(gtx.Ops)
gtx.Dimensions = layout.Dimensions{
Size: image.Point{X: size, Y: size},
}
})
pointer.EllipseAreaOp{Rect: image.Rectangle{Max: gtx.Dimensions.Size}}.Add(gtx.Ops)
button.Layout(gtx)
})
bgcol := b.Background
if !button.Active() {
bgcol.A = 0xaa
}
bg := st.Expand(gtx, func() {
size := float32(gtx.Constraints.Width.Max)
rr := float32(size) * .5
rrect(gtx.Ops,
size,
size,
rr, rr, rr, rr,
)
fill(gtx, bgcol)
for _, c := range button.History() {
drawInk(gtx, c)
}
})
st.Layout(gtx, bg, ico)
}
func (ic *Icon) image(sz int) image.Image {
if sz == ic.imgSize {
return ic.img
}
m, _ := iconvg.DecodeMetadata(ic.src)
dx, dy := m.ViewBox.AspectRatio()
img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, Y: int(float32(sz) * dy / dx)}})
var ico iconvg.Rasterizer
ico.SetDstImage(img, img.Bounds(), draw.Src)
// Use white for icons.
m.Palette[0] = color.RGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}
iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{
Palette: &m.Palette,
})
ic.img = img
ic.imgSize = sz
return img
}
func fab(gtx *layout.Context, ico image.Image, col color.RGBA, size int) {
dp := image.Point{X: (size - ico.Bounds().Dx()) / 2, Y: (size - ico.Bounds().Dy()) / 2}
dims := image.Point{X: size, Y: size}
rr := float32(size) * .5
rrect(gtx.Ops, float32(size), float32(size), rr, rr, rr, rr)
paint.ColorOp{Color: col}.Add(gtx.Ops)
paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: float32(size), Y: float32(size)}}}.Add(gtx.Ops)
paint.ImageOp{Src: ico, Rect: ico.Bounds()}.Add(gtx.Ops)
paint.PaintOp{
Rect: toRectF(ico.Bounds().Add(dp)),
}.Add(gtx.Ops)
gtx.Dimensions = layout.Dimensions{Size: dims}
}
func toRectF(r image.Rectangle) f32.Rectangle {
return f32.Rectangle{
Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)},
}
}
func drawInk(gtx *layout.Context, c widget.Click) {
d := gtx.Now().Sub(c.Time)
t := float32(d.Seconds())
const duration = 0.5
if t > duration {
return
}
t = t / duration
var stack op.StackOp
stack.Push(gtx.Ops)
size := float32(gtx.Px(unit.Dp(700))) * t
rr := size * .5
col := byte(0xaa * (1 - t*t))
ink := paint.ColorOp{Color: color.RGBA{A: col, R: col, G: col, B: col}}
ink.Add(gtx.Ops)
op.TransformOp{}.Offset(c.Position).Offset(f32.Point{
X: -rr,
Y: -rr,
}).Add(gtx.Ops)
rrect(gtx.Ops, float32(size), float32(size), rr, rr, rr, rr)
paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: float32(size), Y: float32(size)}}}.Add(gtx.Ops)
stack.Pop()
op.InvalidateOp{}.Add(gtx.Ops)
}
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Unlicense OR MIT
package material
import (
"image/color"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
)
type Editor struct {
Font text.Font
// Color is the text color.
Color color.RGBA
// Hint contains the text displayed when the editor is empty.
Hint string
// HintColor is the color of hint text.
HintColor color.RGBA
shaper *text.Shaper
}
func (t *Theme) Editor(hint string) Editor {
return Editor{
Font: text.Font{
Size: unit.Sp(16),
},
Color: t.Color.Text,
shaper: t.Shaper,
Hint: hint,
HintColor: t.Color.Hint,
}
}
func (e Editor) Layout(gtx *layout.Context, editor *widget.Editor) {
var stack op.StackOp
stack.Push(gtx.Ops)
var macro op.MacroOp
macro.Record(gtx.Ops)
paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
tl := widget.Label{Alignment: editor.Alignment}
tl.Layout(gtx, e.shaper, e.Font, e.Hint)
macro.Stop()
if w := gtx.Dimensions.Size.X; gtx.Constraints.Width.Min < w {
gtx.Constraints.Width.Min = w
}
if h := gtx.Dimensions.Size.Y; gtx.Constraints.Height.Min < h {
gtx.Constraints.Height.Min = h
}
editor.Layout(gtx, e.shaper, e.Font)
if editor.Len() > 0 {
paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
editor.PaintText(gtx)
} else {
macro.Add(gtx.Ops)
}
paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
editor.PaintCaret(gtx)
stack.Pop()
}
+11 -12
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package widget implements common widgets.
package widget
package material
import (
"image"
@@ -19,22 +18,22 @@ type Image struct {
// Rect is the source rectangle.
Rect image.Rectangle
// Scale is the ratio of image pixels to
// device pixels. If zero, a scale that
// makes the image appear at approximately
// 72 DPI is used.
// dps.
Scale float32
}
func (t *Theme) Image(img image.Image) Image {
return Image{
Src: img,
Rect: img.Bounds(),
Scale: 160 / 72, // About 72 DPI.
}
}
func (im Image) Layout(gtx *layout.Context) {
size := im.Src.Bounds()
wf, hf := float32(size.Dx()), float32(size.Dy())
var w, h int
if im.Scale == 0 {
const dpPrPx = 160 / 72
w, h = gtx.Px(unit.Dp(wf*dpPrPx)), gtx.Px(unit.Dp(hf*dpPrPx))
} else {
w, h = int(wf*im.Scale+.5), int(hf*im.Scale+.5)
}
w, h := gtx.Px(unit.Dp(wf*im.Scale)), gtx.Px(unit.Dp(hf*im.Scale))
cs := gtx.Constraints
d := image.Point{X: cs.Width.Constrain(w), Y: cs.Height.Constrain(h)}
aspect := float32(w) / float32(h)
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Unlicense OR MIT
package material
import (
"image/color"
"gioui.org/layout"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
)
type Label struct {
// Face defines the text style.
Font text.Font
// Color is the text color.
Color color.RGBA
// Alignment specify the text alignment.
Alignment text.Alignment
// MaxLines limits the number of lines. Zero means no limit.
MaxLines int
Text string
shaper *text.Shaper
}
func (t *Theme) H1(txt string) Label {
return t.Label(t.TextSize.Scale(96.0/16.0), txt)
}
func (t *Theme) H2(txt string) Label {
return t.Label(t.TextSize.Scale(60.0/16.0), txt)
}
func (t *Theme) H3(txt string) Label {
return t.Label(t.TextSize.Scale(48.0/16.0), txt)
}
func (t *Theme) H4(txt string) Label {
return t.Label(t.TextSize.Scale(34.0/16.0), txt)
}
func (t *Theme) H5(txt string) Label {
return t.Label(t.TextSize.Scale(24.0/16.0), txt)
}
func (t *Theme) H6(txt string) Label {
return t.Label(t.TextSize.Scale(20.0/16.0), txt)
}
func (t *Theme) Body1(txt string) Label {
return t.Label(t.TextSize, txt)
}
func (t *Theme) Body2(txt string) Label {
return t.Label(t.TextSize.Scale(14.0/16.0), txt)
}
func (t *Theme) Caption(txt string) Label {
return t.Label(t.TextSize.Scale(12.0/16.0), txt)
}
func (t *Theme) Label(size unit.Value, txt string) Label {
return Label{
Text: txt,
Color: t.Color.Text,
Font: text.Font{
Size: size,
},
shaper: t.Shaper,
}
}
func (l Label) Layout(gtx *layout.Context) {
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines}
tl.Layout(gtx, l.shaper, l.Font, l.Text)
}
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package material implements the Material design.
package material
import (
"image"
"image/color"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
)
type Theme struct {
Shaper *text.Shaper
Color struct {
Primary color.RGBA
Text color.RGBA
Hint color.RGBA
}
TextSize unit.Value
}
func NewTheme(shaper *text.Shaper) *Theme {
t := &Theme{
Shaper: shaper,
}
t.Color.Primary = rgb(0x3f51b5)
t.Color.Text = rgb(0x000000)
t.Color.Hint = rgb(0xbbbbbb)
t.TextSize = unit.Sp(16)
return t
}
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 fill(gtx *layout.Context, col color.RGBA) {
cs := gtx.Constraints
d := image.Point{X: cs.Width.Max, Y: cs.Height.Max}
dr := f32.Rectangle{
Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
}
paint.ColorOp{Color: col}.Add(gtx.Ops)
paint.PaintOp{Rect: dr}.Add(gtx.Ops)
gtx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
}
// https://pomax.github.io/bezierinfo/#circles_cubic.
func rrect(ops *op.Ops, width, height, se, sw, nw, ne float32) {
w, h := float32(width), float32(height)
const c = 0.55228475 // 4*(sqrt(2)-1)/3
var b paint.Path
b.Begin(ops)
b.Move(f32.Point{X: w, Y: h - se})
b.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE
b.Line(f32.Point{X: sw - w + se, Y: 0})
b.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW
b.Line(f32.Point{X: 0, Y: nw - h + sw})
b.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW
b.Line(f32.Point{X: w - ne - nw, Y: 0})
b.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE
b.End().Add(ops)
}