~eliasnaur/gio

abb99eca5c54238e34054cacb56936e17cff12a0 — Elias Naur 2 years ago ff3fc7a
theme/material: add the material theme

Signed-off-by: Elias Naur <mail@eliasnaur.com>
9 files changed, 537 insertions(+), 15 deletions(-)

M go.mod
M go.sum
A widget/button.go
A widget/doc.go
A widget/material/button.go
A widget/material/editor.go
R widget/{image.go => material/image.go}
A widget/material/label.go
A widget/material/material.go
M go.mod => go.mod +2 -1
@@ 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
)

M go.sum => go.sum +21 -2
@@ 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=

A widget/button.go => widget/button.go +70 -0
@@ 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]
	}
}

A widget/doc.go => widget/doc.go +6 -0
@@ 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

A widget/material/button.go => widget/material/button.go +209 -0
@@ 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)
}

A widget/material/editor.go => widget/material/editor.go +65 -0
@@ 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()
}

R widget/image.go => widget/material/image.go +11 -12
@@ 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)

A widget/material/label.go => widget/material/label.go +80 -0
@@ 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)
}

A widget/material/material.go => widget/material/material.go +73 -0
@@ 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)
}