~eliasnaur/scatter

376080be3371ee87ae62c7768d8c3f2bdcd5ca42 — Elias Naur 6 months ago 2dbfa0d
cmd/scatter: upgrade to newest Gio

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

M cmd/scatter/ui.go
M go.mod
M go.sum
M cmd/scatter/ui.go => cmd/scatter/ui.go +211 -215
@@ 19,7 19,6 @@ import (
	"gioui.org/ui/app"
	"gioui.org/ui/f32"
	"gioui.org/ui/gesture"
	"gioui.org/ui/input"
	"gioui.org/ui/key"
	"gioui.org/ui/layout"
	"gioui.org/ui/measure"


@@ 71,8 70,8 @@ type pageStack struct {

type Page interface {
	Start(stop <-chan struct{})
	Event(c ui.Config, q input.Queue) interface{}
	Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context)
	Event(c *layout.Context) interface{}
	Layout(c *layout.Context)
}

type signInPage struct {


@@ 224,9 223,9 @@ func colorMaterial(ops *ui.Ops, color color.RGBA) ui.MacroOp {

func (a *App) run() error {
	var updates <-chan struct{}
	ops := new(ui.Ops)
	var cfg ui.Config
	ctx := new(layout.Context)
	c := &layout.Context{
		Queue: a.w.Queue(),
	}
	for {
		select {
		case <-updates:


@@ 273,22 272,19 @@ func (a *App) run() error {
					}
				}
			case app.UpdateEvent:
				ops.Reset()
				cfg = &e.Config
				q := a.w.Queue()
				a.env.faces.Reset(cfg)
				c.Reset(&e.Config, layout.RigidConstraints(e.Size))
				a.env.faces.Reset(c)
				a.env.insets = layout.Inset{
					Top:    e.Insets.Top,
					Left:   e.Insets.Left,
					Right:  e.Insets.Right,
					Bottom: e.Insets.Bottom,
				}
				ctx.Constraints = layout.RigidConstraints(e.Size)
				a.Layout(cfg, q, ops, ctx)
				a.Layout(c)
				if a.profiling {
					a.layoutTimings(cfg, q, ops, ctx)
					a.layoutTimings(c)
				}
				a.w.Update(ops)
				a.w.Update(c.Ops)
			}
		}
	}


@@ 306,13 302,13 @@ func (t *Transition) Start(stop <-chan struct{}) {
	t.page.Start(stop)
}

func (t *Transition) Event(c ui.Config, q input.Queue) interface{} {
	return t.page.Event(c, q)
func (t *Transition) Event(c *layout.Context) interface{} {
	return t.page.Event(c)
}

func (t *Transition) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (t *Transition) Layout(c *layout.Context) {
	var stack ui.StackOp
	stack.Push(ops)
	stack.Push(c.Ops)
	prev, page := t.prev, t.page
	if prev != nil {
		if t.reverse {


@@ 322,8 318,8 @@ func (t *Transition) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout
		if t.time.IsZero() {
			t.time = now
		}
		prev.Layout(c, q, ops, ctx)
		cs := ctx.Constraints
		prev.Layout(c)
		cs := c.Constraints
		size := f32.Point{X: float32(cs.Width.Max), Y: float32(cs.Height.Max)}
		max := float32(math.Sqrt(float64(size.X*size.X + size.Y*size.Y)))
		progress := float32(now.Sub(t.time).Seconds()) * 3


@@ 337,16 333,16 @@ func (t *Transition) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout
		}
		diameter := progress * max
		radius := diameter / 2
		ui.InvalidateOp{}.Add(ops)
		ui.InvalidateOp{}.Add(c.Ops)
		center := size.Mul(.5)
		clipCenter := f32.Point{X: diameter / 2, Y: diameter / 2}
		off := ui.TransformOp{}.Offset(center.Sub(clipCenter))
		off.Add(ops)
		rrect(ops, diameter, diameter, radius, radius, radius, radius)
		off.Invert().Add(ops)
		fill{theme.white}.Layout(ops, ctx)
		off.Add(c.Ops)
		rrect(c.Ops, diameter, diameter, radius, radius, radius, radius)
		off.Invert().Add(c.Ops)
		fill{theme.white}.Layout(c)
	}
	page.Layout(c, q, ops, ctx)
	page.Layout(c)
	stack.Pop()
}



@@ 431,29 427,29 @@ 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 (a *App) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
	a.update(c, q)
	a.stack.Current().Layout(c, q, ops, ctx)
func (a *App) Layout(c *layout.Context) {
	a.update(c)
	a.stack.Current().Layout(c)
}

func (a *App) layoutTimings(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
	for e, ok := q.Next(a); ok; e, ok = q.Next(a) {
func (a *App) layoutTimings(c *layout.Context) {
	for e, ok := c.Next(a); ok; e, ok = c.Next(a) {
		if e, ok := e.(system.ProfileEvent); ok {
			a.profile = e
		}
	}

	system.ProfileOp{Key: a}.Add(ops)
	system.ProfileOp{Key: a}.Add(c.Ops)
	var mstats runtime.MemStats
	runtime.ReadMemStats(&mstats)
	mallocs := mstats.Mallocs - a.lastMallocs
	a.lastMallocs = mstats.Mallocs
	layout.Align(layout.NE).Layout(ops, ctx, func() {
	layout.Align(layout.NE).Layout(c, func() {
		in := a.env.insets
		in.Top = ui.Max(c, ui.Dp(16), in.Top)
		in.Layout(c, ops, ctx, func() {
		in.Layout(c, func() {
			txt := fmt.Sprintf("m: %d %s", mallocs, a.profile.Timings)
			text.Label{Material: theme.text, Face: a.env.faces.For(fonts.mono, ui.Sp(10)), Text: txt}.Layout(ops, ctx)
			text.Label{Material: theme.text, Face: a.env.faces.For(fonts.mono, ui.Sp(10)), Text: txt}.Layout(c)
		})
	})
}


@@ 482,9 478,9 @@ func newContactsPage(env *Env) *contactsPage {

func (p *contactsPage) Start(stop <-chan struct{}) {}

func (p *contactsPage) Event(c ui.Config, q input.Queue) interface{} {
func (p *contactsPage) Event(c *layout.Context) interface{} {
	for {
		e, ok := p.searchEdit.Next(c, q)
		e, ok := p.searchEdit.Next(c)
		if !ok {
			break
		}


@@ 503,13 499,13 @@ func (p *contactsPage) Event(c ui.Config, q input.Queue) interface{} {
	default:
	}
	for i := range p.clicks {
		for e, ok := p.clicks[i].Next(q); ok; e, ok = p.clicks[i].Next(q) {
		for e, ok := p.clicks[i].Next(c); ok; e, ok = p.clicks[i].Next(c) {
			if e.Type == gesture.TypeClick {
				return NewThreadEvent{p.contacts[i].Address}
			}
		}
	}
	return p.topbar.Event(c, q)
	return p.topbar.Event(c)
}

func isEmailAddress(e string) bool {


@@ 537,30 533,30 @@ func (p *contactsPage) queryContacts(q string) {
	}()
}

func (p *contactsPage) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
	for e := p.Event(c, q); e != nil; e = p.Event(c, q) {
func (p *contactsPage) Layout(c *layout.Context) {
	for e := p.Event(c); e != nil; e = p.Event(c) {
	}
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(ops)
		key.HideInputOp{}.Add(c.Ops)
	}
	f := layout.Flex{Axis: layout.Vertical}
	f.Init(ops, ctx)
	f.Init(c)
	c1 := f.Rigid(func() {
		p.topbar.Layout(c, p.env.insets, ops, ctx, func() {
			p.searchEdit.Layout(c, q, ops, ctx)
		p.topbar.Layout(c, p.env.insets, func() {
			p.searchEdit.Layout(c)
		})
	})
	c2 := f.Flexible(1, func() {
		ctx.Constraints.Height.Min = ctx.Constraints.Height.Max
		l.Layout(c, q, ops, ctx, len(p.contacts), func(i int) {
			p.contact(c, ops, ctx, i)
		c.Constraints.Height.Min = c.Constraints.Height.Max
		l.Layout(c, len(p.contacts), func(i int) {
			p.contact(c, i)
		})
	})
	f.Layout(c1, c2)
}

func (p *contactsPage) contact(c ui.Config, ops *ui.Ops, ctx *layout.Context, index int) {
func (p *contactsPage) contact(c *layout.Context, index int) {
	in := layout.Inset{
		Top:    ui.Dp(16),
		Bottom: ui.Dp(16),


@@ 569,31 565,31 @@ func (p *contactsPage) contact(c ui.Config, ops *ui.Ops, ctx *layout.Context, in
	}
	contact := p.contacts[index]
	click := &p.clicks[index]
	in.Layout(c, ops, ctx, func() {
		f := (&layout.Flex{Alignment: layout.Middle}).Init(ops, ctx)
	in.Layout(c, func() {
		f := (&layout.Flex{Alignment: layout.Middle}).Init(c)
		c1 := f.Rigid(func() {
			in := layout.Inset{Right: ui.Dp(8)}
			in.Layout(c, ops, ctx, func() {
			in.Layout(c, func() {
				cc := clipCircle{}
				cc.Layout(ops, ctx, func() {
				cc.Layout(c, func() {
					sz := image.Point{X: c.Px(ui.Dp(48)), Y: c.Px(ui.Dp(48))}
					ctx.Constraints = layout.RigidConstraints(ctx.Constraints.Constrain(sz))
					fill{theme.brand}.Layout(ops, ctx)
					c.Constraints = layout.RigidConstraints(c.Constraints.Constrain(sz))
					fill{theme.brand}.Layout(c)
				})
			})
		})
		c2 := f.Flexible(1, func() {
			text.Label{Material: theme.text, Face: p.env.faces.For(fonts.regular, ui.Sp(18)), Text: contact.Address}.Layout(ops, ctx)
			text.Label{Material: theme.text, Face: p.env.faces.For(fonts.regular, ui.Sp(18)), Text: contact.Address}.Layout(c)
		})

		f.Layout(c1, c2)
	})
	pointer.RectAreaOp{Rect: image.Rectangle{Max: ctx.Dimensions.Size}}.Add(ops)
	click.Add(ops)
	pointer.RectAreaOp{Rect: image.Rectangle{Max: c.Dimensions.Size}}.Add(c.Ops)
	click.Add(c.Ops)
}

func (t *Topbar) Event(c ui.Config, q input.Queue) interface{} {
	for e, ok := t.backClick.Next(q); ok; e, ok = t.backClick.Next(q) {
func (t *Topbar) Event(c *layout.Context) interface{} {
	for e, ok := t.backClick.Next(c); ok; e, ok = t.backClick.Next(c) {
		if e.Type == gesture.TypeClick {
			return BackEvent{}
		}


@@ 601,7 597,7 @@ func (t *Topbar) Event(c ui.Config, q input.Queue) interface{} {
	return nil
}

func (t *Topbar) Layout(c ui.Config, insets layout.Inset, ops *ui.Ops, ctx *layout.Context, w layout.Widget) {
func (t *Topbar) Layout(c *layout.Context, insets layout.Inset, w layout.Widget) {
	stack := layout.Stack{Alignment: layout.SW}
	insets = layout.Inset{
		Top:    ui.Add(c, insets.Top, ui.Dp(16)),


@@ 609,18 605,18 @@ func (t *Topbar) Layout(c ui.Config, insets layout.Inset, ops *ui.Ops, ctx *layo
		Left:   ui.Max(c, insets.Left, ui.Dp(16)),
		Right:  ui.Max(c, insets.Right, ui.Dp(16)),
	}
	stack.Init(ops, ctx)
	stack.Init(c)
	stackContent := stack.Rigid(func() {
		insets.Layout(c, ops, ctx, func() {
		insets.Layout(c, func() {
			flex := layout.Flex{Alignment: layout.Middle}
			flex.Init(ops, ctx)
			flex.Init(c)
			backChild := flex.Rigid(func() {
				if t.Back {
					ico := (&icon{src: icons.NavigationArrowBack, size: ui.Dp(24)}).image(c, rgb(0xffffff))
					widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(c, ops, ctx)
					ctx.Dimensions.Size.X += c.Px(ui.Dp(4))
					pointer.RectAreaOp{Rect: image.Rectangle{Max: ctx.Dimensions.Size}}.Add(ops)
					t.backClick.Add(ops)
					widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(c)
					c.Dimensions.Size.X += c.Px(ui.Dp(4))
					pointer.RectAreaOp{Rect: image.Rectangle{Max: c.Dimensions.Size}}.Add(c.Ops)
					t.backClick.Add(c.Ops)
				}
			})
			content := flex.Flexible(1, w)


@@ 628,7 624,7 @@ func (t *Topbar) Layout(c ui.Config, insets layout.Inset, ops *ui.Ops, ctx *layo
		})
	})
	bg := stack.Expand(func() {
		fill{theme.brand}.Layout(ops, ctx)
		fill{theme.brand}.Layout(c)
	})
	stack.Layout(bg, stackContent)
}


@@ 672,8 668,8 @@ func newSignInPage(env *Env) *signInPage {
func (p *signInPage) Start(stop <-chan struct{}) {
}

func (p *signInPage) Event(c ui.Config, q input.Queue) interface{} {
	for e, ok := p.submit.Next(q); ok; e, ok = p.submit.Next(q) {
func (p *signInPage) Event(c *layout.Context) interface{} {
	for e, ok := p.submit.Next(c); ok; e, ok = p.submit.Next(c) {
		if e.Type == gesture.TypeClick {
			for _, f := range p.fields {
				*f.Value = f.edit.Text()


@@ 684,30 680,30 @@ func (p *signInPage) Event(c ui.Config, q input.Queue) interface{} {
	return nil
}

func (p *signInPage) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (p *signInPage) Layout(c *layout.Context) {
	f := layout.Flex{Axis: layout.Vertical}
	f.Init(ops, ctx)
	f.Init(c)

	c1 := f.Rigid(func() {
		var t Topbar
		t.Layout(c, p.env.insets, ops, ctx, func() {
			text.Label{Material: colorMaterial(ops, rgb(0xffffff)), Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: "Sign in"}.Layout(ops, ctx)
		t.Layout(c, p.env.insets, func() {
			text.Label{Material: colorMaterial(c.Ops, rgb(0xffffff)), Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: "Sign in"}.Layout(c)
		})
	})

	c2 := f.Flexible(1, func() {
		p.layoutSigninForm(c, q, ops, ctx)
		p.layoutSigninForm(c)
	})
	f.Layout(c1, c2)
}

func (p *signInPage) layoutSigninForm(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (p *signInPage) layoutSigninForm(c *layout.Context) {
	l := p.list
	inset := layout.Inset{
		Left:  ui.Max(c, ui.Dp(32), p.env.insets.Left),
		Right: ui.Max(c, ui.Dp(32), p.env.insets.Right),
	}
	l.Layout(c, q, ops, ctx, len(p.fields)+1, func(i int) {
	l.Layout(c, len(p.fields)+1, func(i int) {
		in := inset
		switch {
		case i < len(p.fields):


@@ 715,52 711,52 @@ func (p *signInPage) layoutSigninForm(c ui.Config, q input.Queue, ops *ui.Ops, c
			if i == 0 {
				in.Top = ui.Dp(32)
			}
			in.Layout(c, ops, ctx, func() {
				p.fields[i].Layout(c, q, ops, ctx)
			in.Layout(c, func() {
				p.fields[i].Layout(c)
			})
		default:
			in.Bottom = ui.Max(c, ui.Dp(32), p.env.insets.Bottom)
			layout.Align(layout.E).Layout(ops, ctx, func() {
				in.Layout(c, ops, ctx, func() {
					p.submit.Layout(c, p.env, ops, ctx)
			layout.Align(layout.E).Layout(c, func() {
				in.Layout(c, func() {
					p.submit.Layout(c, p.env)
				})
			})
		}
	})
}

func (f *formField) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
	theme.text.Add(ops)
	fl := (&layout.Flex{Axis: layout.Vertical}).Init(ops, ctx)
func (f *formField) Layout(c *layout.Context) {
	theme.text.Add(c.Ops)
	fl := (&layout.Flex{Axis: layout.Vertical}).Init(c)

	header := text.Label{Material: theme.text, Text: f.Header, Face: f.env.faces.For(fonts.bold, ui.Sp(12))}
	c1 := fl.Rigid(func() {
		ctx.Constraints.Width.Min = ctx.Constraints.Width.Max
		header.Layout(ops, ctx)
		ctx.Dimensions.Size.Y += c.Px(ui.Dp(4))
		c.Constraints.Width.Min = c.Constraints.Width.Max
		header.Layout(c)
		c.Dimensions.Size.Y += c.Px(ui.Dp(4))
	})
	c2 := fl.Rigid(func() {
		f.edit.Layout(c, q, ops, ctx)
		f.edit.Layout(c)
	})
	fl.Layout(c1, c2)
}

func (b *Button) Next(q input.Queue) (gesture.ClickEvent, bool) {
func (b *Button) Next(q ui.Queue) (gesture.ClickEvent, bool) {
	return b.click.Next(q)
}

func (b *Button) Layout(c ui.Config, env *Env, ops *ui.Ops, ctx *layout.Context) {
func (b *Button) Layout(c *layout.Context, env *Env) {
	bg := Background{
		Material: theme.brand,
		Radius:   ui.Dp(4),
		Inset:    layout.UniformInset(ui.Dp(8)),
	}
	bg.Layout(c, ops, ctx, func() {
	bg.Layout(c, func() {
		lbl := text.Label{Material: theme.white, Face: env.faces.For(fonts.regular, ui.Sp(16)), Text: b.Label, Alignment: text.Middle}
		lbl.Layout(ops, ctx)
		lbl.Layout(c)
	})
	pointer.RectAreaOp{Rect: image.Rectangle{Max: ctx.Dimensions.Size}}.Add(ops)
	b.click.Add(ops)
	pointer.RectAreaOp{Rect: image.Rectangle{Max: c.Dimensions.Size}}.Add(c.Ops)
	b.click.Add(c.Ops)
}

type Background struct {


@@ 769,14 765,14 @@ type Background struct {
	Inset    layout.Inset
}

func (b *Background) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context, w layout.Widget) {
func (b *Background) Layout(c *layout.Context, w layout.Widget) {
	var macro ui.MacroOp
	macro.Record(ops)
	b.Inset.Layout(c, ops, ctx, w)
	macro.Record(c.Ops)
	b.Inset.Layout(c, w)
	macro.Stop()
	var stack ui.StackOp
	stack.Push(ops)
	size := ctx.Dimensions.Size
	stack.Push(c.Ops)
	size := c.Dimensions.Size
	width, height := float32(size.X), float32(size.Y)
	if r := float32(c.Px(b.Radius)); r > 0 {
		if r > width/2 {


@@ 785,11 781,11 @@ func (b *Background) Layout(c ui.Config, ops *ui.Ops, ctx *layout.Context, w lay
		if r > height/2 {
			r = height / 2
		}
		rrect(ops, width, height, r, r, r, r)
		rrect(c.Ops, width, height, r, r, r, r)
	}
	b.Material.Add(ops)
	paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: width, Y: height}}}.Add(ops)
	macro.Add(ops)
	b.Material.Add(c.Ops)
	paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: width, Y: height}}}.Add(c.Ops)
	macro.Add(c.Ops)
	stack.Pop()
}



@@ 816,7 812,7 @@ func (p *threadsPage) Start(stop <-chan struct{}) {
	}()
}

func (p *threadsPage) Event(c ui.Config, q input.Queue) interface{} {
func (p *threadsPage) Event(c *layout.Context) interface{} {
	select {
	case <-p.updates:
		p.fetchThreads()


@@ 826,14 822,14 @@ func (p *threadsPage) Event(c ui.Config, q input.Queue) interface{} {
		p.env.redraw()
	default:
	}
	for e, ok := p.fab.Next(q); ok; e, ok = p.fab.Next(q) {
	for e, ok := p.fab.Next(c); ok; e, ok = p.fab.Next(c) {
		if e.Type == gesture.TypeClick {
			return ShowContactsEvent{}
		}
	}
	for i := range p.clicks {
		click := &p.clicks[i]
		for e, ok := click.Next(q); ok; e, ok = click.Next(q) {
		for e, ok := click.Next(c); ok; e, ok = click.Next(c) {
			if e.Type == gesture.TypeClick {
				t := p.threads[i]
				return ShowThreadEvent{Thread: t.ID}


@@ 856,45 852,45 @@ func (p *threadsPage) fetchThreads() {
	}()
}

func (p *threadsPage) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (p *threadsPage) Layout(c *layout.Context) {
	st := layout.Stack{Alignment: layout.Center}
	st.Init(ops, ctx)
	st.Init(c)

	c1 := st.Rigid(func() {
		f := layout.Flex{Axis: layout.Vertical}
		f.Init(ops, ctx)
		f.Init(c)

		c1 := f.Rigid(func() {
			var t Topbar
			t.Layout(c, p.env.insets, ops, ctx, func() {
				text.Label{Material: theme.white, Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: p.account.User}.Layout(ops, ctx)
			t.Layout(c, p.env.insets, func() {
				text.Label{Material: theme.white, Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: p.account.User}.Layout(c)
			})
		})

		c2 := f.Flexible(1, func() {
			p.layoutThreads(c, q, ops, ctx)
			p.layoutThreads(c)
		})
		f.Layout(c1, c2)
	})
	c2 := st.Rigid(func() {
		layout.Align(layout.SE).Layout(ops, ctx, func() {
		layout.Align(layout.SE).Layout(c, func() {
			layout.Inset{
				Right:  ui.Max(c, ui.Dp(16), p.env.insets.Right),
				Bottom: ui.Max(c, ui.Dp(16), p.env.insets.Bottom),
			}.Layout(c, ops, ctx, func() {
				p.fab.Layout(c, p.env, ops, ctx)
			}.Layout(c, func() {
				p.fab.Layout(c)
			})
		})
	})
	st.Layout(c1, c2)
}

func (p *threadsPage) layoutThreads(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (p *threadsPage) layoutThreads(c *layout.Context) {
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(ops)
		key.HideInputOp{}.Add(c.Ops)
	}
	l.Layout(c, q, ops, ctx, len(p.threads), func(i int) {
	l.Layout(c, len(p.threads), func(i int) {
		in := layout.Inset{}
		switch i {
		case 0:


@@ 902,8 898,8 @@ func (p *threadsPage) layoutThreads(c ui.Config, q input.Queue, ops *ui.Ops, ctx
		case len(p.threads) - 1:
			in.Bottom = ui.Max(c, ui.Dp(4), p.env.insets.Bottom)
		}
		in.Layout(c, ops, ctx, func() {
			p.thread(c, ops, ctx, i)
		in.Layout(c, func() {
			p.thread(c, i)
		})
	})
}


@@ 917,7 913,7 @@ var contactColors = []color.RGBA{
	{A: 0xff, R: 0x00, G: 0x89, B: 0x7b},
}

func (p *threadsPage) thread(c ui.Config, ops *ui.Ops, ctx *layout.Context, index int) {
func (p *threadsPage) thread(c *layout.Context, index int) {
	t := p.threads[index]
	bgtexmat := theme.tertText
	font := fonts.regular


@@ 930,29 926,29 @@ func (p *threadsPage) thread(c ui.Config, ops *ui.Ops, ctx *layout.Context, inde
		Left:  ui.Max(c, ui.Dp(16), p.env.insets.Left),
		Right: ui.Max(c, ui.Dp(16), p.env.insets.Right),
	}
	in.Layout(c, ops, ctx, func() {
	in.Layout(c, func() {
		elem := layout.Flex{Axis: layout.Vertical}
		elem.Init(ops, ctx)
		elem.Init(c)
		c1 := elem.Rigid(func() {
			in := layout.Inset{Top: ui.Dp(8), Bottom: ui.Dp(8)}
			in.Layout(c, ops, ctx, func() {
			in.Layout(c, func() {
				f := centerRowOpts()
				f.Init(ops, ctx)
				f.Init(c)
				c1 := f.Rigid(func() {
					in := layout.Inset{Right: ui.Dp(12)}
					cc := clipCircle{}
					in.Layout(c, ops, ctx, func() {
						cc.Layout(ops, ctx, func() {
					in.Layout(c, func() {
						cc.Layout(c, func() {
							st := layout.Stack{Alignment: layout.Center}
							st.Init(ops, ctx)
							st.Init(c)

							// Background color
							c1 := st.Rigid(func() {
								sz := image.Point{X: c.Px(ui.Dp(48)), Y: c.Px(ui.Dp(48))}
								ctx.Constraints = layout.RigidConstraints(ctx.Constraints.Constrain(sz))
								c.Constraints = layout.RigidConstraints(c.Constraints.Constrain(sz))
								color := contactColors[index%len(contactColors)]
								mat := colorMaterial(ops, color)
								fill{mat}.Layout(ops, ctx)
								mat := colorMaterial(c.Ops, color)
								fill{mat}.Layout(c)
							})

							// Contact initial.


@@ 963,7 959,7 @@ func (p *threadsPage) thread(c ui.Config, ops *ui.Ops, ctx *layout.Context, inde
									break
								}
								face := p.env.faces.For(fonts.regular, ui.Sp(24))
								text.Label{Material: theme.white, Face: face, Text: initial}.Layout(ops, ctx)
								text.Label{Material: theme.white, Face: face, Text: initial}.Layout(c)
							})
							st.Layout(c1, c2)
						})


@@ 971,39 967,39 @@ func (p *threadsPage) thread(c ui.Config, ops *ui.Ops, ctx *layout.Context, inde
				})
				c2 := f.Rigid(func() {
					f := column()
					f.Init(ops, ctx)
					f.Init(c)
					c1 := f.Rigid(func() {
						f := baseline()
						f.Init(ops, ctx)
						f.Init(c)
						c1 := f.Rigid(func() {
							text.Label{Material: theme.text, Face: p.env.faces.For(font, ui.Sp(18)), Text: t.ID}.Layout(ops, ctx)
							text.Label{Material: theme.text, Face: p.env.faces.For(font, ui.Sp(18)), Text: t.ID}.Layout(c)
						})
						c2 := f.Flexible(1, func() {
							ctx.Constraints.Width.Min = ctx.Constraints.Width.Max
							c.Constraints.Width.Min = c.Constraints.Width.Max
							in := layout.Inset{Left: ui.Dp(2)}
							in.Layout(c, ops, ctx, func() {
							in.Layout(c, func() {
								text.Label{
									Alignment: text.End,
									Material:  bgtexmat,
									Face:      p.env.faces.For(font, ui.Sp(12)),
									Text:      formatTime(t.Updated),
								}.Layout(ops, ctx)
								}.Layout(c)
							})
						})
						f.Layout(c1, c2)
					})
					c2 := f.Rigid(func() {
						in := layout.Inset{Top: ui.Dp(6)}
						in.Layout(c, ops, ctx, func() {
							text.Label{Material: bgtexmat, Face: p.env.faces.For(font, ui.Sp(14)), MaxLines: 1, Text: t.Snippet}.Layout(ops, ctx)
						in.Layout(c, func() {
							text.Label{Material: bgtexmat, Face: p.env.faces.For(font, ui.Sp(14)), MaxLines: 1, Text: t.Snippet}.Layout(c)
						})
					})
					f.Layout(c1, c2)
				})
				f.Layout(c1, c2)
			})
			pointer.RectAreaOp{Rect: image.Rectangle{Max: ctx.Dimensions.Size}}.Add(ops)
			click.Add(ops)
			pointer.RectAreaOp{Rect: image.Rectangle{Max: c.Dimensions.Size}}.Add(c.Ops)
			click.Add(c.Ops)
		})
		elem.Layout(c1)
	})


@@ 1053,23 1049,23 @@ func (p *threadPage) Start(stop <-chan struct{}) {
	}()
}

func (p *threadPage) Event(c ui.Config, q input.Queue) interface{} {
func (p *threadPage) Event(c *layout.Context) interface{} {
	select {
	case <-p.updates:
		p.fetchMessages()
	default:
	}
	for e, ok := p.msgEdit.Next(c, q); ok; e, ok = p.msgEdit.Next(c, q) {
	for e, ok := p.msgEdit.Next(c); ok; e, ok = p.msgEdit.Next(c) {
		if _, ok := e.(text.SubmitEvent); ok {
			p.sendMessage()
		}
	}
	for e, ok := p.send.Next(q); ok; e, ok = p.send.Next(q) {
	for e, ok := p.send.Next(c); ok; e, ok = p.send.Next(c) {
		if e.Type == gesture.TypeClick {
			p.sendMessage()
		}
	}
	for e, ok := p.invite.Next(q); ok; e, ok = p.invite.Next(q) {
	for e, ok := p.invite.Next(c); ok; e, ok = p.invite.Next(c) {
		if e.Type == gesture.TypeClick {
			if err := p.env.client.Send(p.thread.ID, "Invitation sent"); err != nil {
				log.Printf("failed to send invitation: %v", err)


@@ 1077,7 1073,7 @@ func (p *threadPage) Event(c ui.Config, q input.Queue) interface{} {
			break
		}
	}
	for e, ok := p.accept.Next(q); ok; e, ok = p.accept.Next(q) {
	for e, ok := p.accept.Next(c); ok; e, ok = p.accept.Next(c) {
		if e.Type == gesture.TypeClick {
			if err := p.env.client.Send(p.thread.ID, "Invitation accepted"); err != nil {
				log.Printf("failed to send invitation accept: %v", err)


@@ 1085,7 1081,7 @@ func (p *threadPage) Event(c ui.Config, q input.Queue) interface{} {
			break
		}
	}
	return p.topbar.Event(c, q)
	return p.topbar.Event(c)
}

func (p *threadPage) sendMessage() {


@@ 1097,20 1093,20 @@ func (p *threadPage) sendMessage() {
	}
}

func (p *threadPage) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
func (p *threadPage) Layout(c *layout.Context) {
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(ops)
		key.HideInputOp{}.Add(c.Ops)
	}
	select {
	case p.messages = <-p.result:
	default:
	}
	f := layout.Flex{Axis: layout.Vertical}
	f.Init(ops, ctx)
	f.Init(c)
	c1 := f.Rigid(func() {
		p.topbar.Layout(c, p.env.insets, ops, ctx, func() {
			text.Label{Material: theme.white, Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: p.thread.ID}.Layout(ops, ctx)
		p.topbar.Layout(c, p.env.insets, func() {
			text.Label{Material: theme.white, Face: p.env.faces.For(fonts.regular, ui.Sp(20)), Text: p.thread.ID}.Layout(c)
		})
	})



@@ 1121,121 1117,121 @@ func (p *threadPage) Layout(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout
			Right:  ui.Max(c, ui.Dp(16), p.env.insets.Right),
			Bottom: ui.Max(c, ui.Dp(16), p.env.insets.Bottom),
		}
		in.Layout(c, ops, ctx, func() {
		in.Layout(c, func() {
			switch {
			case p.thread.PendingInvitation:
				p.accept.Layout(c, p.env, ops, ctx)
				p.accept.Layout(c, p.env)
			case p.env.client.ContainsSession(p.thread.ID):
				p.layoutMessageBox(c, q, ops, ctx)
				p.layoutMessageBox(c)
			default:
				p.invite.Layout(c, p.env, ops, ctx)
				p.invite.Layout(c, p.env)
			}
		})
	})

	c2 := f.Flexible(1, func() {
		ctx.Constraints.Height.Min = ctx.Constraints.Height.Max
		l.Layout(c, q, ops, ctx, len(p.messages), func(i int) {
			p.message(c, ops, ctx, i)
		c.Constraints.Height.Min = c.Constraints.Height.Max
		l.Layout(c, len(p.messages), func(i int) {
			p.message(c, i)
		})
	})
	f.Layout(c1, c2, c3)
}

func (p *threadPage) layoutMessageBox(c ui.Config, q input.Queue, ops *ui.Ops, ctx *layout.Context) {
	if mh := c.Px(ui.Dp(100)); ctx.Constraints.Height.Max > mh {
		ctx.Constraints.Height.Max = mh
func (p *threadPage) layoutMessageBox(c *layout.Context) {
	if mh := c.Px(ui.Dp(100)); c.Constraints.Height.Max > mh {
		c.Constraints.Height.Max = mh
	}
	f := (&layout.Flex{Alignment: layout.End}).Init(ops, ctx)
	f := (&layout.Flex{Alignment: layout.End}).Init(c)

	var sendHeight int
	c2 := f.Rigid(func() {
		in := layout.Inset{Left: ui.Dp(8)}
		in.Layout(c, ops, ctx, func() {
			p.send.Layout(c, p.env, ops, ctx)
			sendHeight = ctx.Dimensions.Size.Y
		in.Layout(c, func() {
			p.send.Layout(c)
			sendHeight = c.Dimensions.Size.Y
		})
	})

	c1 := f.Flexible(1, func() {
		ctx.Constraints.Width.Min = ctx.Constraints.Width.Max
		if ctx.Constraints.Height.Min < sendHeight {
			ctx.Constraints.Height.Min = sendHeight
		c.Constraints.Width.Min = c.Constraints.Width.Max
		if c.Constraints.Height.Min < sendHeight {
			c.Constraints.Height.Min = sendHeight
		}
		bg := Background{
			Material: colorMaterial(ops, rgb(0xeeeeee)),
			Material: colorMaterial(c.Ops, rgb(0xeeeeee)),
			Inset:    layout.UniformInset(ui.Dp(8)),
			Radius:   ui.Dp(10),
		}
		bg.Layout(c, ops, ctx, func() {
			layout.Align(layout.W).Layout(ops, ctx, func() {
				ctx.Constraints.Width.Min = ctx.Constraints.Width.Max
				p.msgEdit.Layout(c, q, ops, ctx)
		bg.Layout(c, func() {
			layout.Align(layout.W).Layout(c, func() {
				c.Constraints.Width.Min = c.Constraints.Width.Max
				p.msgEdit.Layout(c)
			})
		})
	})
	f.Layout(c1, c2)
}

func (p *threadPage) message(c ui.Config, ops *ui.Ops, ctx *layout.Context, index int) {
func (p *threadPage) message(c *layout.Context, index int) {
	msg := p.messages[index]
	in := layout.Inset{Top: ui.Dp(16), Left: ui.Dp(16), Right: ui.Dp(40)}
	align := layout.Align(layout.W)
	msgMat := colorMaterial(ops, rgb(0xffffff))
	msgMat := colorMaterial(c.Ops, rgb(0xffffff))
	bgcol := theme.brand
	timecol := argb(0xaaaaaaaa)
	if msg.Own {
		in.Left, in.Right = in.Right, in.Left
		align = layout.Align(layout.E)
		bgcol = colorMaterial(ops, rgb(0xeeeeee))
		bgcol = colorMaterial(c.Ops, rgb(0xeeeeee))
		msgMat = theme.text
		timecol = rgb(0x888888)
	}
	in.Left = ui.Max(c, in.Left, p.env.insets.Left)
	in.Right = ui.Max(c, in.Right, p.env.insets.Right)
	in.Layout(c, ops, ctx, func() {
		align.Layout(ops, ctx, func() {
	in.Layout(c, func() {
		align.Layout(c, func() {
			bg := Background{
				Material: bgcol,
				Inset:    layout.Inset{Top: ui.Dp(8), Bottom: ui.Dp(8), Left: ui.Dp(12), Right: ui.Dp(12)},
				Radius:   ui.Dp(10),
			}
			bg.Layout(c, ops, ctx, func() {
			bg.Layout(c, func() {
				f := layout.Flex{Axis: layout.Vertical}
				f.Init(ops, ctx)
				f.Init(c)

				var msgWidth int
				c1 := f.Rigid(func() {
					label := text.Label{Material: msgMat, Face: p.env.faces.For(fonts.regular, ui.Sp(14)), Text: msg.Message}
					label.Layout(ops, ctx)
					ctx.Dimensions.Size.Y += c.Px(ui.Dp(4))
					msgWidth = ctx.Dimensions.Size.X
					label.Layout(c)
					c.Dimensions.Size.Y += c.Px(ui.Dp(4))
					msgWidth = c.Dimensions.Size.X
				})

				c2 := f.Rigid(func() {
					ctx.Constraints.Width.Min = msgWidth
					c.Constraints.Width.Min = msgWidth
					f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}
					f.Init(ops, ctx)
					f.Init(c)

					var children []layout.FlexChild
					child := f.Rigid(func() {
						time := formatTime(msg.Time)
						tlbl := text.Label{Material: colorMaterial(ops, timecol), Face: p.env.faces.For(fonts.regular, ui.Sp(10)), Text: time}
						tlbl.Layout(ops, ctx)
						tlbl := text.Label{Material: colorMaterial(c.Ops, timecol), Face: p.env.faces.For(fonts.regular, ui.Sp(10)), Text: time}
						tlbl.Layout(c)
					})
					children = append(children, child)

					if msg.Own {
						child := f.Rigid(func() {
							in := layout.Inset{Left: ui.Dp(12)}
							in.Layout(c, ops, ctx, func() {
							in.Layout(c, func() {
								checkmark := p.checkmark.image(c, timecol)
								r := checkmark.Bounds()
								if msg.Sent {
									paint.ImageOp{Src: checkmark, Rect: r}.Add(ops)
									paint.PaintOp{Rect: toRectF(r)}.Add(ops)
									paint.ImageOp{Src: checkmark, Rect: r}.Add(c.Ops)
									paint.PaintOp{Rect: toRectF(r)}.Add(c.Ops)
								}
								ctx.Dimensions = layout.Dimensions{Size: r.Size()}
								c.Dimensions = layout.Dimensions{Size: r.Size()}
							})
						})
						children = append(children, child)


@@ 1278,29 1274,29 @@ func formatTime(t time.Time) string {
	return t.Format(format)
}

func (b *IconButton) Next(q input.Queue) (gesture.ClickEvent, bool) {
func (b *IconButton) Next(q ui.Queue) (gesture.ClickEvent, bool) {
	return b.click.Next(q)
}

func (b *IconButton) Layout(c ui.Config, env *Env, ops *ui.Ops, ctx *layout.Context) {
func (b *IconButton) Layout(c *layout.Context) {
	ico := b.Icon.image(c, rgb(0xffffff))
	bg := Background{
		Material: theme.brand,
		Radius:   ui.Px(1e6),
		Inset:    b.Inset,
	}
	bg.Layout(c, ops, ctx, func() {
	bg.Layout(c, func() {
		sz := image.Point{X: ico.Bounds().Dx(), Y: ico.Bounds().Dy()}
		ctx.Constraints = layout.RigidConstraints(ctx.Constraints.Constrain(sz))
		widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(c, ops, ctx)
		c.Constraints = layout.RigidConstraints(c.Constraints.Constrain(sz))
		widget.Image{Src: ico, Rect: ico.Bounds(), Scale: 1}.Layout(c)
	})
	pointer.EllipseAreaOp{Rect: image.Rectangle{Max: ctx.Dimensions.Size}}.Add(ops)
	b.click.Add(ops)
	pointer.EllipseAreaOp{Rect: image.Rectangle{Max: c.Dimensions.Size}}.Add(c.Ops)
	b.click.Add(c.Ops)
}

func (a *App) update(c ui.Config, q input.Queue) {
func (a *App) update(c *layout.Context) {
	page := a.stack.Current()
	if e := page.Event(c, q); e != nil {
	if e := page.Event(c); e != nil {
		switch e := e.(type) {
		case BackEvent:
			a.stack.Pop()


@@ 1321,15 1317,15 @@ type fill struct {
	material ui.MacroOp
}

func (f fill) Layout(ops *ui.Ops, ctx *layout.Context) {
	cs := ctx.Constraints
func (f fill) Layout(c *layout.Context) {
	cs := c.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)},
	}
	f.material.Add(ops)
	paint.PaintOp{Rect: dr}.Add(ops)
	ctx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
	f.material.Add(c.Ops)
	paint.PaintOp{Rect: dr}.Add(c.Ops)
	c.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
}

func column() layout.Flex {


@@ 1347,11 1343,11 @@ func baseline() layout.Flex {
type clipCircle struct {
}

func (c *clipCircle) Layout(ops *ui.Ops, ctx *layout.Context, w layout.Widget) {
func (cc *clipCircle) Layout(c *layout.Context, w layout.Widget) {
	var macro ui.MacroOp
	macro.Record(ops)
	macro.Record(c.Ops)
	w()
	dims := ctx.Dimensions
	dims := c.Dimensions
	macro.Stop()
	max := dims.Size.X
	if dy := dims.Size.Y; dy > max {


@@ 1360,9 1356,9 @@ func (c *clipCircle) Layout(ops *ui.Ops, ctx *layout.Context, w layout.Widget) {
	szf := float32(max)
	rr := szf * .5
	var stack ui.StackOp
	stack.Push(ops)
	rrect(ops, szf, szf, rr, rr, rr, rr)
	macro.Add(ops)
	stack.Push(c.Ops)
	rrect(c.Ops, szf, szf, rr, rr, rr, rr)
	macro.Add(c.Ops)
	stack.Pop()
}


M go.mod => go.mod +1 -1
@@ 3,7 3,7 @@ module scatter.im
go 1.13

require (
	gioui.org/ui v0.0.0-20190924183532-b928ee65f7a2
	gioui.org/ui v0.0.0-20190924202912-4d84f46edbb1
	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

M go.sum => go.sum +2 -2
@@ 1,5 1,5 @@
gioui.org/ui v0.0.0-20190924183532-b928ee65f7a2 h1:98ei56UOalfOxmm5R/On3CtpsbgDAvvhIrLyxnaApuY=
gioui.org/ui v0.0.0-20190924183532-b928ee65f7a2/go.mod h1:PssKPKlqVIeyaed+0w492Xc2NgX5M3n6oZKOAj5rxoE=
gioui.org/ui v0.0.0-20190924202912-4d84f46edbb1 h1:jdvrOtvYqUZYuFXV6dsttppqvlvewPFeGgfV2nC0Gd8=
gioui.org/ui v0.0.0-20190924202912-4d84f46edbb1/go.mod h1:PssKPKlqVIeyaed+0w492Xc2NgX5M3n6oZKOAj5rxoE=
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=