9816373c847c8309a83b641eb6624cdeb49101dd — Elias Naur a month ago ef6e719
cmd/scatter: update gio version

Signed-off-by: Elias Naur <mail@eliasnaur.com>
3 files changed, 159 insertions(+), 224 deletions(-)

M cmd/scatter/ui.go
M go.mod
M go.sum
M cmd/scatter/ui.go => cmd/scatter/ui.go +143 -212
@@ 25,18 25,18 @@ import (
 	"gioui.org/op"
 	"gioui.org/op/paint"
 	"gioui.org/text"
-	"gioui.org/text/shape"
+	"gioui.org/text/opentype"
 	"gioui.org/unit"
 	"gioui.org/widget"
+	"gioui.org/widget/material"
 	"golang.org/x/exp/shiny/iconvg"
+	"golang.org/x/exp/shiny/materialdesign/icons"
 
 	"golang.org/x/image/font/gofont/gobold"
 	"golang.org/x/image/font/gofont/goitalic"
 	"golang.org/x/image/font/gofont/gomono"
 	"golang.org/x/image/font/gofont/goregular"
 	"golang.org/x/image/font/sfnt"
-
-	"golang.org/x/exp/shiny/materialdesign/icons"
 )
 
 type Env struct {


@@ 79,12 79,7 @@ type signInPage struct {
 	account *Account
 	list    *layout.List
 	fields  []*formField
-	submit  *Button
-}
-
-type Button struct {
-	Label string
-	buttonState
+	submit  *widget.Button
 }
 
 type Topbar struct {


@@ 98,14 93,15 @@ type formField struct {
 	Header string
 	Hint   string
 	Value  *string
-	edit   *text.Editor
+	edit   *widget.Editor
 }
 
 type threadsPage struct {
 	env     *Env
 	account *Account
 
-	fab *IconButton
+	fab     *widget.Button
+	fabIcon *material.Icon
 
 	updates       <-chan struct{}
 	threadUpdates chan []*Thread


@@ 123,10 119,10 @@ type threadPage struct {
 	list      *layout.List
 	messages  []*Message
 	result    chan []*Message
-	msgEdit   *text.Editor
-	send      *IconButton
-	invite    *Button
-	accept    *Button
+	msgEdit   *widget.Editor
+	send      *widget.Button
+	invite    *widget.Button
+	accept    *widget.Button
 	topbar    *Topbar
 	updates   <-chan struct{}
 }


@@ 134,7 130,7 @@ type threadPage struct {
 type contactsPage struct {
 	env        *Env
 	list       *layout.List
-	searchEdit *text.Editor
+	searchEdit *widget.Editor
 	contacts   []*Contact
 	clicks     []gesture.Click
 	query      chan []*Contact


@@ 154,17 150,6 @@ type icon struct {
 	imgSize int
 }
 
-type IconButton struct {
-	Icon  *icon
-	Inset layout.Inset
-	buttonState
-}
-
-type buttonState struct {
-	click  gesture.Click
-	clicks int
-}
-
 type BackEvent struct{}
 
 type SignInEvent struct {


@@ 181,32 166,31 @@ type ShowThreadEvent struct {
 	Thread string
 }
 
-var families struct {
-	primary *shape.Family
-	mono    *shape.Family
-}
+var theme *material.Theme
 
-var theme struct {
-	text     op.MacroOp
-	tertText op.MacroOp
-	brand    op.MacroOp
-	white    op.MacroOp
+var iconLib struct {
+	create *material.Icon
+	send   *material.Icon
 }
 
 func uiMain() {
-	families.primary = &shape.Family{
-		Regular: mustLoadFont(goregular.TTF),
-		Italic:  mustLoadFont(goitalic.TTF),
-		Bold:    mustLoadFont(gobold.TTF),
-	}
-	families.mono = &shape.Family{
-		Regular: mustLoadFont(gomono.TTF),
-	}
-	var ops op.Ops
-	theme.text = colorMaterial(&ops, rgb(0x000000))
-	theme.tertText = colorMaterial(&ops, rgb(0xbbbbbb))
-	theme.brand = colorMaterial(&ops, rgb(0x3c98c6))
-	theme.white = colorMaterial(&ops, rgb(0xffffff))
+	shaper := new(text.Shaper)
+	shaper.Register(text.Font{}, opentype.Must(
+		opentype.Parse(goregular.TTF),
+	))
+	shaper.Register(text.Font{Style: text.Italic}, opentype.Must(
+		opentype.Parse(goitalic.TTF),
+	))
+	shaper.Register(text.Font{Weight: text.Bold}, opentype.Must(
+		opentype.Parse(gobold.TTF),
+	))
+	shaper.Register(text.Font{Typeface: "mono"}, opentype.Must(
+		opentype.Parse(gomono.TTF),
+	))
+	theme = material.NewTheme(shaper)
+	theme.Color.Primary = rgb(0x3c98c6)
+	iconLib.create = mustIcon(icons.ContentCreate)
+	iconLib.send = mustIcon(icons.ContentSend)
 	go func() {
 		w := app.NewWindow(
 			app.Size(unit.Dp(400), unit.Dp(800)),


@@ 219,12 203,12 @@ func uiMain() {
 	app.Main()
 }
 
-func colorMaterial(ops *op.Ops, color color.RGBA) op.MacroOp {
-	var mat op.MacroOp
-	mat.Record(ops)
-	paint.ColorOp{Color: color}.Add(ops)
-	mat.Stop()
-	return mat
+func mustIcon(data []byte) *material.Icon {
+	ico, err := material.NewIcon(data)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return ico
 }
 
 func (a *App) run() error {


@@ 277,7 261,7 @@ func (a *App) run() error {
 						a.w.Invalidate()
 					}
 				}
-			case app.UpdateEvent:
+			case app.FrameEvent:
 				gtx.Reset(&e.Config, e.Size)
 				a.env.insets = layout.Inset{
 					Top:    e.Insets.Top,


@@ 289,7 273,7 @@ func (a *App) run() error {
 				if a.profiling {
 					a.layoutTimings(gtx)
 				}
-				a.w.Update(gtx.Ops)
+				e.Frame(gtx.Ops)
 			}
 		}
 	}


@@ 345,7 329,7 @@ func (t *Transition) Layout(gtx *layout.Context) {
 		off.Add(gtx.Ops)
 		rrect(gtx.Ops, diameter, diameter, radius, radius, radius, radius)
 		off.Invert().Add(gtx.Ops)
-		fill{theme.white}.Layout(gtx)
+		fill{rgb(0xffffff)}.Layout(gtx)
 	}
 	page.Layout(gtx)
 	stack.Pop()


@@ 454,7 438,9 @@ func (a *App) layoutTimings(gtx *layout.Context) {
 		in.Top = unit.Max(gtx, unit.Dp(16), in.Top)
 		in.Layout(gtx, func() {
 			txt := fmt.Sprintf("m: %d %s", mallocs, a.profile.Timings)
-			text.Label{Material: theme.text, Size: unit.Sp(10), Text: txt}.Layout(gtx, families.mono)
+			lbl := theme.Caption(txt)
+			lbl.Font.Typeface = "mono"
+			lbl.Layout(gtx)
 		})
 	})
 }


@@ 465,14 451,9 @@ func newContactsPage(env *Env) *contactsPage {
 		list: &layout.List{
 			Axis: layout.Vertical,
 		},
-		searchEdit: &text.Editor{
-			Family:       families.primary,
-			Size:         unit.Sp(20),
-			SingleLine:   true,
-			Submit:       true,
-			Hint:         "Email address",
-			Material:     theme.white,
-			HintMaterial: theme.tertText,
+		searchEdit: &widget.Editor{
+			SingleLine: true,
+			Submit:     true,
 		},
 		topbar: &Topbar{
 			Back: true,


@@ 485,15 466,12 @@ func newContactsPage(env *Env) *contactsPage {
 func (p *contactsPage) Start(stop <-chan struct{}) {}
 
 func (p *contactsPage) Event(gtx *layout.Context) interface{} {
-	for e, ok := p.searchEdit.Event(gtx); ok; e, ok = p.searchEdit.Event(gtx) {
-		if !ok {
-			break
-		}
-		switch e.(type) {
-		case text.ChangeEvent:
+	for _, e := range p.searchEdit.Events(gtx) {
+		switch e := e.(type) {
+		case widget.ChangeEvent:
 			p.queryContacts(p.searchEdit.Text())
-		case text.SubmitEvent:
-			if t := p.searchEdit.Text(); isEmailAddress(t) {
+		case widget.SubmitEvent:
+			if t := e.Text; isEmailAddress(t) {
 				return NewThreadEvent{Address: t}
 			}
 		}


@@ 548,7 526,11 @@ func (p *contactsPage) Layout(gtx *layout.Context) {
 	f := layout.Flex{Axis: layout.Vertical}
 	c1 := f.Rigid(gtx, func() {
 		p.topbar.Layout(gtx, p.env.insets, func() {
-			p.searchEdit.Layout(gtx)
+			e := theme.Editor("Email address")
+			e.Font.Size = unit.Sp(20)
+			e.Color = rgb(0xffffff)
+			e.HintColor = rgb(0xbbbbbb)
+			e.Layout(gtx, p.searchEdit)
 		})
 	})
 	c2 := f.Flex(gtx, 1, func() {


@@ 578,12 560,12 @@ func (p *contactsPage) contact(gtx *layout.Context, index int) {
 				cc.Layout(gtx, func() {
 					sz := image.Point{X: gtx.Px(unit.Dp(48)), Y: gtx.Px(unit.Dp(48))}
 					gtx.Constraints = layout.RigidConstraints(gtx.Constraints.Constrain(sz))
-					fill{theme.brand}.Layout(gtx)
+					fill{theme.Color.Primary}.Layout(gtx)
 				})
 			})
 		})
 		c2 := f.Flex(gtx, 1, func() {
-			text.Label{Material: theme.text, Size: unit.Sp(18), Text: contact.Address}.Layout(gtx, families.primary)
+			theme.H6(contact.Address).Layout(gtx)
 		})
 
 		f.Layout(gtx, c1, c2)


@@ 615,7 597,9 @@ func (t *Topbar) Layout(gtx *layout.Context, insets layout.Inset, w layout.Widge
 			backChild := flex.Rigid(gtx, func() {
 				if t.Back {
 					ico := (&icon{src: icons.NavigationArrowBack, size: unit.Dp(24)}).image(gtx, rgb(0xffffff))
-					widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(gtx)
+					img := theme.Image(ico)
+					img.Scale = 1
+					img.Layout(gtx)
 					gtx.Dimensions.Size.X += gtx.Px(unit.Dp(4))
 					pointer.RectAreaOp{Rect: image.Rectangle{Max: gtx.Dimensions.Size}}.Add(gtx.Ops)
 					t.backClick.Add(gtx.Ops)


@@ 626,7 610,7 @@ func (t *Topbar) Layout(gtx *layout.Context, insets layout.Inset, w layout.Widge
 		})
 	})
 	bg := stack.Expand(gtx, func() {
-		fill{theme.brand}.Layout(gtx)
+		fill{theme.Color.Primary}.Layout(gtx)
 	})
 	stack.Layout(gtx, bg, stackContent)
 }


@@ 649,19 633,12 @@ func newSignInPage(env *Env) *signInPage {
 			&formField{Header: "IMAP host", Hint: "host:port", Value: &acc.IMAPHost},
 			&formField{Header: "SMTP host", Hint: "host:port", Value: &acc.SMTPHost},
 		},
-		submit: &Button{
-			Label: "Sign in",
-		},
+		submit: &widget.Button{},
 	}
 	for _, f := range p.fields {
 		f.env = p.env
-		f.edit = &text.Editor{
-			Family:       families.primary,
-			Size:         unit.Sp(16),
-			SingleLine:   true,
-			Hint:         f.Hint,
-			Material:     theme.text,
-			HintMaterial: theme.tertText,
+		f.edit = &widget.Editor{
+			SingleLine: true,
 		}
 		f.edit.SetText(*f.Value)
 	}


@@ 687,7 664,9 @@ func (p *signInPage) Layout(gtx *layout.Context) {
 	c1 := f.Rigid(gtx, func() {
 		var t Topbar
 		t.Layout(gtx, p.env.insets, func() {
-			text.Label{Material: colorMaterial(gtx.Ops, rgb(0xffffff)), Size: unit.Sp(20), Text: "Sign in"}.Layout(gtx, families.primary)
+			lbl := theme.H6("Sign in")
+			lbl.Color = rgb(0xffffff)
+			lbl.Layout(gtx)
 		})
 	})
 


@@ 718,7 697,7 @@ func (p *signInPage) layoutSigninForm(gtx *layout.Context) {
 			in.Bottom = unit.Max(gtx, unit.Dp(32), p.env.insets.Bottom)
 			layout.Align(layout.E).Layout(gtx, func() {
 				in.Layout(gtx, func() {
-					p.submit.Layout(gtx, p.env)
+					theme.Button("Sign in").Layout(gtx, p.submit)
 				})
 			})
 		}


@@ 726,58 705,25 @@ func (p *signInPage) layoutSigninForm(gtx *layout.Context) {
 }
 
 func (f *formField) Layout(gtx *layout.Context) {
-	theme.text.Add(gtx.Ops)
 	fl := layout.Flex{Axis: layout.Vertical}
 
-	header := text.Label{Material: theme.text, Text: f.Header, Size: unit.Sp(12)}
-	header.Face.Weight = text.Bold
 	c1 := fl.Rigid(gtx, func() {
 		gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
-		header.Layout(gtx, families.primary)
+		header := theme.Caption(f.Header)
+		header.Font.Weight = text.Bold
+		header.Layout(gtx)
 		gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
 	})
 	c2 := fl.Rigid(gtx, func() {
-		f.edit.Layout(gtx)
+		theme.Editor(f.Hint).Layout(gtx, f.edit)
 	})
 	fl.Layout(gtx, c1, c2)
 }
 
-func (b *buttonState) Clicked(gtx *layout.Context) bool {
-	for _, e := range b.click.Events(gtx) {
-		if e.Type == gesture.TypeClick {
-			b.clicks++
-		}
-	}
-	if b.clicks > 0 {
-		b.clicks--
-		return true
-	}
-	return false
-}
-
-func (b *buttonState) clear() {
-	b.clicks = 0
-}
-
-func (b *Button) Layout(gtx *layout.Context, env *Env) {
-	b.buttonState.clear()
-	bg := Background{
-		Material: theme.brand,
-		Radius:   unit.Dp(4),
-		Inset:    layout.UniformInset(unit.Dp(8)),
-	}
-	bg.Layout(gtx, func() {
-		lbl := text.Label{Material: theme.white, Size: unit.Sp(16), Text: b.Label, Alignment: text.Middle}
-		lbl.Layout(gtx, families.primary)
-	})
-	pointer.RectAreaOp{Rect: image.Rectangle{Max: gtx.Dimensions.Size}}.Add(gtx.Ops)
-	b.click.Add(gtx.Ops)
-}
-
 type Background struct {
-	Material op.MacroOp
-	Radius   unit.Value
-	Inset    layout.Inset
+	Color  color.RGBA
+	Radius unit.Value
+	Inset  layout.Inset
 }
 
 func (b *Background) Layout(gtx *layout.Context, w layout.Widget) {


@@ 798,7 744,7 @@ func (b *Background) Layout(gtx *layout.Context, w layout.Widget) {
 		}
 		rrect(gtx.Ops, width, height, r, r, r, r)
 	}
-	b.Material.Add(gtx.Ops)
+	paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
 	paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: width, Y: height}}}.Add(gtx.Ops)
 	macro.Add(gtx.Ops)
 	stack.Pop()


@@ 810,10 756,7 @@ func newThreadsPage(env *Env) *threadsPage {
 		list: &layout.List{
 			Axis: layout.Vertical,
 		},
-		fab: &IconButton{
-			Icon:  &icon{src: icons.ContentCreate, size: unit.Dp(24)},
-			Inset: layout.UniformInset(unit.Dp(16)),
-		},
+		fab: new(widget.Button),
 	}
 }
 


@@ 874,7 817,9 @@ func (p *threadsPage) Layout(gtx *layout.Context) {
 		c1 := f.Rigid(gtx, func() {
 			var t Topbar
 			t.Layout(gtx, p.env.insets, func() {
-				text.Label{Material: theme.white, Size: unit.Sp(20), Text: p.account.User}.Layout(gtx, families.primary)
+				lbl := theme.H6(p.account.User)
+				lbl.Color = rgb(0xffffff)
+				lbl.Layout(gtx)
 			})
 		})
 


@@ 889,6 834,7 @@ func (p *threadsPage) Layout(gtx *layout.Context) {
 				Right:  unit.Max(gtx, unit.Dp(16), p.env.insets.Right),
 				Bottom: unit.Max(gtx, unit.Dp(16), p.env.insets.Bottom),
 			}.Layout(gtx, func() {
+				theme.IconButton(iconLib.create).Layout(gtx, p.fab)
 				p.fab.Layout(gtx)
 			})
 		})


@@ 926,11 872,11 @@ var contactColors = []color.RGBA{
 
 func (p *threadsPage) thread(gtx *layout.Context, index int) {
 	t := p.threads[index]
-	bgtexmat := theme.tertText
-	face := text.Face{}
+	bgtexcol := rgb(0xbbbbbb)
+	fontWeight := text.Normal
 	if t.Unread > 0 {
-		bgtexmat = theme.text
-		face.Weight = text.Bold
+		bgtexcol = theme.Color.Text
+		fontWeight = text.Bold
 	}
 	click := &p.clicks[index]
 	in := layout.Inset{


@@ 955,8 901,7 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
 								sz := image.Point{X: gtx.Px(unit.Dp(48)), Y: gtx.Px(unit.Dp(48))}
 								gtx.Constraints = layout.RigidConstraints(gtx.Constraints.Constrain(sz))
 								color := contactColors[index%len(contactColors)]
-								mat := colorMaterial(gtx.Ops, color)
-								fill{mat}.Layout(gtx)
+								fill{color}.Layout(gtx)
 							})
 
 							// Contact initial.


@@ 966,7 911,9 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
 									initial = string(unicode.ToUpper(c))
 									break
 								}
-								text.Label{Material: theme.white, Size: unit.Sp(24), Text: initial}.Layout(gtx, families.primary)
+								lbl := theme.H5(initial)
+								lbl.Color = rgb(0xffffff)
+								lbl.Layout(gtx)
 							})
 							st.Layout(gtx, c1, c2)
 						})


@@ 977,19 924,19 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
 					c1 := f.Rigid(gtx, func() {
 						f := baseline()
 						c1 := f.Rigid(gtx, func() {
-							text.Label{Material: theme.text, Face: face, Size: unit.Sp(18), Text: t.ID}.Layout(gtx, families.primary)
+							lbl := theme.H6(t.ID)
+							lbl.Font.Weight = fontWeight
+							lbl.Layout(gtx)
 						})
 						c2 := f.Flex(gtx, 1, func() {
 							gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
 							in := layout.Inset{Left: unit.Dp(2)}
 							in.Layout(gtx, func() {
-								text.Label{
-									Alignment: text.End,
-									Material:  bgtexmat,
-									Face:      face,
-									Size:      unit.Sp(12),
-									Text:      formatTime(t.Updated),
-								}.Layout(gtx, families.primary)
+								lbl := theme.Caption(formatTime(t.Updated))
+								lbl.Color = bgtexcol
+								lbl.Alignment = text.End
+								lbl.Font.Weight = fontWeight
+								lbl.Layout(gtx)
 							})
 						})
 						f.Layout(gtx, c1, c2)


@@ 997,7 944,11 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
 					c2 := f.Rigid(gtx, func() {
 						in := layout.Inset{Top: unit.Dp(6)}
 						in.Layout(gtx, func() {
-							text.Label{Material: bgtexmat, Face: face, Size: unit.Sp(14), MaxLines: 1, Text: t.Snippet}.Layout(gtx, families.primary)
+							lbl := theme.Body2(t.Snippet)
+							lbl.Color = bgtexcol
+							lbl.Font.Weight = fontWeight
+							lbl.MaxLines = 1
+							lbl.Layout(gtx)
 						})
 					})
 					f.Layout(gtx, c1, c2)


@@ 1022,24 973,12 @@ func newThreadPage(env *Env, threadID string) *threadPage {
 			ScrollToEnd: true,
 		},
 		result: make(chan []*Message, 1),
-		msgEdit: &text.Editor{
-			Family:       families.primary,
-			Size:         unit.Sp(14),
-			Submit:       true,
-			Hint:         "Send a message",
-			Material:     theme.text,
-			HintMaterial: theme.tertText,
-		},
-		send: &IconButton{
-			Icon:  &icon{src: icons.ContentSend, size: unit.Dp(24)},
-			Inset: layout.UniformInset(unit.Dp(6)),
-		},
-		invite: &Button{
-			Label: "Send invitation",
-		},
-		accept: &Button{
-			Label: "Accept invitation",
+		msgEdit: &widget.Editor{
+			Submit: true,
 		},
+		send:   new(widget.Button),
+		invite: new(widget.Button),
+		accept: new(widget.Button),
 		topbar: &Topbar{
 			Back: true,
 		},


@@ 1062,8 1001,8 @@ func (p *threadPage) Event(gtx *layout.Context) interface{} {
 		p.fetchMessages()
 	default:
 	}
-	for e, ok := p.msgEdit.Event(gtx); ok; e, ok = p.msgEdit.Event(gtx) {
-		if _, ok := e.(text.SubmitEvent); ok {
+	for _, e := range p.msgEdit.Events(gtx) {
+		if _, ok := e.(widget.SubmitEvent); ok {
 			p.sendMessage()
 		}
 	}


@@ 1104,7 1043,9 @@ func (p *threadPage) Layout(gtx *layout.Context) {
 	f := layout.Flex{Axis: layout.Vertical}
 	c1 := f.Rigid(gtx, func() {
 		p.topbar.Layout(gtx, p.env.insets, func() {
-			text.Label{Material: theme.white, Size: unit.Sp(20), Text: p.thread.ID}.Layout(gtx, families.primary)
+			lbl := theme.H6(p.thread.ID)
+			lbl.Color = rgb(0xffffff)
+			lbl.Layout(gtx)
 		})
 	})
 


@@ 1118,11 1059,11 @@ func (p *threadPage) Layout(gtx *layout.Context) {
 		in.Layout(gtx, func() {
 			switch {
 			case p.thread.PendingInvitation:
-				p.accept.Layout(gtx, p.env)
+				theme.Button("Accept invitation").Layout(gtx, p.accept)
 			case p.env.client.ContainsSession(p.thread.ID):
 				p.layoutMessageBox(gtx)
 			default:
-				p.invite.Layout(gtx, p.env)
+				theme.Button("Send invitation").Layout(gtx, p.invite)
 			}
 		})
 	})


@@ 1146,7 1087,10 @@ func (p *threadPage) layoutMessageBox(gtx *layout.Context) {
 	c2 := f.Rigid(gtx, func() {
 		in := layout.Inset{Left: unit.Dp(8)}
 		in.Layout(gtx, func() {
-			p.send.Layout(gtx)
+			btn := theme.IconButton(iconLib.send)
+			btn.Size = unit.Dp(30)
+			btn.Padding = unit.Dp(6)
+			btn.Layout(gtx, p.send)
 			sendHeight = gtx.Dimensions.Size.Y
 		})
 	})


@@ 1157,14 1101,16 @@ func (p *threadPage) layoutMessageBox(gtx *layout.Context) {
 			gtx.Constraints.Height.Min = sendHeight
 		}
 		bg := Background{
-			Material: colorMaterial(gtx.Ops, rgb(0xeeeeee)),
-			Inset:    layout.UniformInset(unit.Dp(8)),
-			Radius:   unit.Dp(10),
+			Color:  rgb(0xeeeeee),
+			Inset:  layout.UniformInset(unit.Dp(8)),
+			Radius: unit.Dp(10),
 		}
 		bg.Layout(gtx, func() {
 			layout.Align(layout.W).Layout(gtx, func() {
 				gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
-				p.msgEdit.Layout(gtx)
+				ed := theme.Editor("Send a message")
+				ed.Font.Size = unit.Sp(14)
+				ed.Layout(gtx, p.msgEdit)
 			})
 		})
 	})


@@ 1175,14 1121,14 @@ func (p *threadPage) message(gtx *layout.Context, index int) {
 	msg := p.messages[index]
 	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
 	align := layout.Align(layout.W)
-	msgMat := colorMaterial(gtx.Ops, rgb(0xffffff))
-	bgcol := theme.brand
+	msgCol := rgb(0xffffff)
+	bgcol := theme.Color.Primary
 	timecol := argb(0xaaaaaaaa)
 	if msg.Own {
 		in.Left, in.Right = in.Right, in.Left
 		align = layout.Align(layout.E)
-		bgcol = colorMaterial(gtx.Ops, rgb(0xeeeeee))
-		msgMat = theme.text
+		bgcol = rgb(0xeeeeee)
+		msgCol = theme.Color.Text
 		timecol = rgb(0x888888)
 	}
 	in.Left = unit.Max(gtx, in.Left, p.env.insets.Left)


@@ 1190,17 1136,18 @@ func (p *threadPage) message(gtx *layout.Context, index int) {
 	in.Layout(gtx, func() {
 		align.Layout(gtx, func() {
 			bg := Background{
-				Material: bgcol,
-				Inset:    layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8), Left: unit.Dp(12), Right: unit.Dp(12)},
-				Radius:   unit.Dp(10),
+				Color:  bgcol,
+				Inset:  layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8), Left: unit.Dp(12), Right: unit.Dp(12)},
+				Radius: unit.Dp(10),
 			}
 			bg.Layout(gtx, func() {
 				f := layout.Flex{Axis: layout.Vertical}
 
 				var msgWidth int
 				c1 := f.Rigid(gtx, func() {
-					label := text.Label{Material: msgMat, Size: unit.Sp(14), Text: msg.Message}
-					label.Layout(gtx, families.primary)
+					lbl := theme.Body2(msg.Message)
+					lbl.Color = msgCol
+					lbl.Layout(gtx)
 					gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
 					msgWidth = gtx.Dimensions.Size.X
 				})


@@ 1212,8 1159,9 @@ func (p *threadPage) message(gtx *layout.Context, index int) {
 					var children []layout.FlexChild
 					child := f.Rigid(gtx, func() {
 						time := formatTime(msg.Time)
-						tlbl := text.Label{Material: colorMaterial(gtx.Ops, timecol), Size: unit.Sp(10), Text: time}
-						tlbl.Layout(gtx, families.primary)
+						lbl := theme.Caption(time)
+						lbl.Color = timecol
+						lbl.Layout(gtx)
 					})
 					children = append(children, child)
 


@@ 1270,23 1218,6 @@ func formatTime(t time.Time) string {
 	return t.Format(format)
 }
 
-func (b *IconButton) Layout(gtx *layout.Context) {
-	b.buttonState.clear()
-	ico := b.Icon.image(gtx, rgb(0xffffff))
-	bg := Background{
-		Material: theme.brand,
-		Radius:   unit.Px(1e6),
-		Inset:    b.Inset,
-	}
-	bg.Layout(gtx, func() {
-		sz := image.Point{X: ico.Bounds().Dx(), Y: ico.Bounds().Dy()}
-		gtx.Constraints = layout.RigidConstraints(gtx.Constraints.Constrain(sz))
-		widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(gtx)
-	})
-	pointer.EllipseAreaOp{Rect: image.Rectangle{Max: gtx.Dimensions.Size}}.Add(gtx.Ops)
-	b.click.Add(gtx.Ops)
-}
-
 func (a *App) update(gtx *layout.Context) {
 	page := a.stack.Current()
 	if e := page.Event(gtx); e != nil {


@@ 1307,7 1238,7 @@ func (a *App) update(gtx *layout.Context) {
 }
 
 type fill struct {
-	material op.MacroOp
+	color color.RGBA
 }
 
 func (f fill) Layout(gtx *layout.Context) {


@@ 1316,7 1247,7 @@ func (f fill) Layout(gtx *layout.Context) {
 	dr := f32.Rectangle{
 		Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
 	}
-	f.material.Add(gtx.Ops)
+	paint.ColorOp{Color: f.color}.Add(gtx.Ops)
 	paint.PaintOp{Rect: dr}.Add(gtx.Ops)
 	gtx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
 }


@@ 1395,5 1326,5 @@ func rrect(ops *op.Ops, width, height, se, sw, nw, ne float32) {
 	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()
+	b.End().Add(ops)
 }

M go.mod => go.mod +2 -2
@@ 3,7 3,7 @@ module scatter.im
 go 1.13
 
 require (
-	gioui.org v0.0.0-20191007100451-370ff4bcc9d6
+	gioui.org v0.0.0-20191012123625-abb99eca5c54
 	github.com/eliasnaur/libsignal-protocol-go v0.0.0-20190626062856-3295f72b181e
 	github.com/emersion/go-imap v1.0.0-rc.1
 	github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e


@@ 12,6 12,6 @@ require (
 	github.com/emersion/go-smtp v0.11.1
 	github.com/stretchr/testify v1.3.0 // indirect
 	go.etcd.io/bbolt v1.3.3
-	golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f
+	golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
 )

M go.sum => go.sum +14 -10
@@ 1,5 1,6 @@
-gioui.org v0.0.0-20191007100451-370ff4bcc9d6 h1:C4aHNuMJHLOZPCaQGW2b9k1xJ9v8+/RyQh4AfvR+3aM=
-gioui.org v0.0.0-20191007100451-370ff4bcc9d6/go.mod h1:+CEjc9B//HrBfWsQOVxjCyih7HGIj3Pww1xFHVDZyyk=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+gioui.org v0.0.0-20191012123625-abb99eca5c54 h1:1LQMwF6UgHRaAbwvoaKWmnv5LwKt2HyXJooDvkNkmRg=
+gioui.org v0.0.0-20191012123625-abb99eca5c54/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/RadicalApp/complete v0.0.0-20170329192659-17e6c0ee499b h1:cAULFohNVfNzco0flF4okSPg3s7/tCj+hMIldtYZo4c=
 github.com/RadicalApp/complete v0.0.0-20170329192659-17e6c0ee499b/go.mod h1:zZ3+l0EkpT2ZPnoamPBG50PBUtQrXwwyJ6elQZMmqgk=


@@ 22,6 23,7 @@ github.com/emersion/go-smtp v0.11.1 h1:2IBWhU2zjrfOOmZal3qRxVsfYnf0rN+ccImZrjnMT
 github.com/emersion/go-smtp v0.11.1/go.mod h1:CfUbM5NgspbOMHFEgCdoK2PVrKt48HAPtL8hnahwfYg=
 github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
 github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=


@@ 38,19 40,20 @@ go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 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/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
 golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f h1:F3VDpCbV+46wJMDIwbFSefCwLlvK2CoEKVEYHO8p5Os=
-golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+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-20190703141733-d6a02ce849c9 h1:uc17S921SPw5F2gJo7slQ3aqvr2RwpL7eb3+DZncu3s=
-golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 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-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+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=


@@ 60,4 63,5 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+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=