~eliasnaur/scatter

76579da69444a4df31344e9e5897df933b213255 — Elias Naur 5 months ago 62deef4
cmd/scatter: bump gio version

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

M cmd/scatter/ui.go
M go.mod
M go.sum
M cmd/scatter/ui.go => cmd/scatter/ui.go +202 -205
@@ 68,8 68,8 @@ type pageStack struct {

type Page interface {
	Start(stop <-chan struct{})
	Event(gtx *layout.Context) interface{}
	Layout(gtx *layout.Context)
	Event(gtx layout.Context) interface{}
	Layout(gtx layout.Context) layout.Dimensions
}

type signInPage struct {


@@ 77,7 77,7 @@ type signInPage struct {
	account *Account
	list    *layout.List
	fields  []*formField
	submit  *widget.Button
	submit  *widget.Clickable
}

type Topbar struct {


@@ 98,7 98,7 @@ type threadsPage struct {
	env     *Env
	account *Account

	fab     *widget.Button
	fab     *widget.Clickable
	fabIcon *widget.Icon

	updates       <-chan struct{}


@@ 118,9 118,9 @@ type threadPage struct {
	messages  []*Message
	result    chan []*Message
	msgEdit   *widget.Editor
	send      *widget.Button
	invite    *widget.Button
	accept    *widget.Button
	send      *widget.Clickable
	invite    *widget.Clickable
	accept    *widget.Clickable
	topbar    *Topbar
	updates   <-chan struct{}
}


@@ 171,6 171,11 @@ var iconLib struct {
	send   *widget.Icon
}

type (
	C = layout.Context
	D = layout.Dimensions
)

func uiMain() {
	gofont.Register()
	theme = material.NewTheme()


@@ 199,7 204,7 @@ func mustIcon(data []byte) *widget.Icon {

func (a *App) run() error {
	var updates <-chan struct{}
	gtx := layout.NewContext(a.w.Queue())
	var ops op.Ops
	for {
		select {
		case <-updates:


@@ 246,7 251,7 @@ func (a *App) run() error {
					}
				}
			case system.FrameEvent:
				gtx.Reset(e.Config, e.Size)
				gtx := layout.NewContext(&ops, e.Queue, e.Config, e.Size)
				a.env.insets = layout.Inset{
					Top:    e.Insets.Top,
					Left:   e.Insets.Left,


@@ 275,13 280,14 @@ func (t *Transition) Start(stop <-chan struct{}) {
	t.page.Start(stop)
}

func (t *Transition) Event(gtx *layout.Context) interface{} {
func (t *Transition) Event(gtx layout.Context) interface{} {
	return t.page.Event(gtx)
}

func (t *Transition) Layout(gtx *layout.Context) {
func (t *Transition) Layout(gtx layout.Context) layout.Dimensions {
	var stack op.StackOp
	stack.Push(gtx.Ops)
	defer stack.Pop()
	prev, page := t.prev, t.page
	if prev != nil {
		if t.reverse {


@@ 293,7 299,7 @@ func (t *Transition) Layout(gtx *layout.Context) {
		}
		prev.Layout(gtx)
		cs := gtx.Constraints
		size := f32.Point{X: float32(cs.Width.Max), Y: float32(cs.Height.Max)}
		size := layout.FPt(cs.Max)
		max := float32(math.Sqrt(float64(size.X*size.X + size.Y*size.Y)))
		progress := float32(now.Sub(t.time).Seconds()) * 3
		progress = progress * progress // Accelerate


@@ 318,8 324,7 @@ func (t *Transition) Layout(gtx *layout.Context) {
		off.Invert().Add(gtx.Ops)
		fill{rgb(0xffffff)}.Layout(gtx)
	}
	page.Layout(gtx)
	stack.Pop()
	return page.Layout(gtx)
}

func (s *pageStack) Len() int {


@@ 403,31 408,31 @@ 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(gtx *layout.Context) {
func (a *App) Layout(gtx layout.Context) {
	a.update(gtx)
	a.stack.Current().Layout(gtx)
}

func (a *App) layoutTimings(gtx *layout.Context) {
func (a *App) layoutTimings(gtx layout.Context) layout.Dimensions {
	for _, e := range gtx.Events(a) {
		if e, ok := e.(profile.Event); ok {
			a.profile = e
		}
	}

	profile.Op{Key: a}.Add(gtx.Ops)
	profile.Op{Tag: a}.Add(gtx.Ops)
	var mstats runtime.MemStats
	runtime.ReadMemStats(&mstats)
	mallocs := mstats.Mallocs - a.lastMallocs
	a.lastMallocs = mstats.Mallocs
	layout.NE.Layout(gtx, func() {
	return layout.NE.Layout(gtx, func(gtx C) D {
		in := a.env.insets
		in.Top = unit.Max(gtx, unit.Dp(16), in.Top)
		in.Layout(gtx, func() {
		return in.Layout(gtx, func(gtx C) D {
			txt := fmt.Sprintf("m: %d %s", mallocs, a.profile.Timings)
			lbl := material.Caption(theme, txt)
			lbl.Font.Variant = "Mono"
			lbl.Layout(gtx)
			return lbl.Layout(gtx)
		})
	})
}


@@ 452,7 457,7 @@ func newContactsPage(env *Env) *contactsPage {

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

func (p *contactsPage) Event(gtx *layout.Context) interface{} {
func (p *contactsPage) Event(gtx layout.Context) interface{} {
	for _, e := range p.searchEdit.Events(gtx) {
		switch e := e.(type) {
		case widget.ChangeEvent:


@@ 503,33 508,33 @@ func (p *contactsPage) queryContacts(q string) {
	}()
}

func (p *contactsPage) Layout(gtx *layout.Context) {
func (p *contactsPage) Layout(gtx layout.Context) layout.Dimensions {
	for e := p.Event(gtx); e != nil; e = p.Event(gtx) {
	}
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(gtx.Ops)
	}
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
			p.topbar.Layout(gtx, p.env.insets, func() {
				e := material.Editor(theme, "Email address")
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx C) D {
			return p.topbar.Layout(gtx, p.env.insets, func(gtx C) D {
				e := material.Editor(theme, p.searchEdit, "Email address")
				e.TextSize = unit.Sp(20)
				e.Color = rgb(0xffffff)
				e.HintColor = rgb(0xbbbbbb)
				e.Layout(gtx, p.searchEdit)
				return e.Layout(gtx)
			})
		}),
		layout.Flexed(1, func() {
			gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
			l.Layout(gtx, len(p.contacts), func(i int) {
				p.contact(gtx, i)
		layout.Flexed(1, func(gtx C) D {
			gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
			return l.Layout(gtx, len(p.contacts), func(gtx C, i int) D {
				return p.contact(gtx, i)
			})
		}),
	)
}

func (p *contactsPage) contact(gtx *layout.Context, index int) {
func (p *contactsPage) contact(gtx layout.Context, index int) layout.Dimensions {
	in := layout.Inset{
		Top:    unit.Dp(16),
		Bottom: unit.Dp(16),


@@ 538,29 543,28 @@ func (p *contactsPage) contact(gtx *layout.Context, index int) {
	}
	contact := p.contacts[index]
	click := &p.clicks[index]
	in.Layout(gtx, func() {
		layout.Flex{Alignment: layout.Middle}.Layout(gtx,
			layout.Rigid(func() {
	dims := in.Layout(gtx, func(gtx C) D {
		return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
			layout.Rigid(func(gtx C) D {
				in := layout.Inset{Right: unit.Dp(8)}
				in.Layout(gtx, func() {
				return in.Layout(gtx, func(gtx C) D {
					cc := clipCircle{}
					cc.Layout(gtx, func() {
					return cc.Layout(gtx, func(gtx C) D {
						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.Color.Primary}.Layout(gtx)
						gtx.Constraints = layout.Exact(gtx.Constraints.Constrain(sz))
						return fill{theme.Color.Primary}.Layout(gtx)
					})
				})
			}),
			layout.Flexed(1, func() {
				material.H6(theme, contact.Address).Layout(gtx)
			}),
			layout.Flexed(1, material.H6(theme, contact.Address).Layout),
		)
	})
	pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
	pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
	click.Add(gtx.Ops)
	return dims
}

func (t *Topbar) Event(gtx *layout.Context) interface{} {
func (t *Topbar) Event(gtx layout.Context) interface{} {
	for _, e := range t.backClick.Events(gtx) {
		if e.Type == gesture.TypeClick {
			return BackEvent{}


@@ 569,30 573,30 @@ func (t *Topbar) Event(gtx *layout.Context) interface{} {
	return nil
}

func (t *Topbar) Layout(gtx *layout.Context, insets layout.Inset, w layout.Widget) {
func (t *Topbar) Layout(gtx layout.Context, insets layout.Inset, w layout.Widget) layout.Dimensions {
	insets = layout.Inset{
		Top:    unit.Add(gtx, insets.Top, unit.Dp(16)),
		Bottom: unit.Dp(16),
		Left:   unit.Max(gtx, insets.Left, unit.Dp(16)),
		Right:  unit.Max(gtx, insets.Right, unit.Dp(16)),
	}
	layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(func() {
			fill{theme.Color.Primary}.Layout(gtx)
		}),
		layout.Stacked(func() {
			insets.Layout(gtx, func() {
				layout.Flex{Alignment: layout.Middle}.Layout(gtx,
					layout.Rigid(func() {
						if t.Back {
							ico := (&icon{src: icons.NavigationArrowBack, size: unit.Dp(24)}).image(gtx, rgb(0xffffff))
							ico.Add(gtx.Ops)
							paint.PaintOp{Rect: f32.Rectangle{Max: toPointF(ico.Size())}}.Add(gtx.Ops)
							gtx.Dimensions.Size = ico.Size()
							gtx.Dimensions.Size.X += gtx.Px(unit.Dp(4))
							pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
							t.backClick.Add(gtx.Ops)
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(fill{theme.Color.Primary}.Layout),
		layout.Stacked(func(gtx C) D {
			return insets.Layout(gtx, func(gtx C) D {
				return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
					layout.Rigid(func(gtx C) D {
						if !t.Back {
							return layout.Dimensions{}
						}
						ico := (&icon{src: icons.NavigationArrowBack, size: unit.Dp(24)}).image(gtx, rgb(0xffffff))
						ico.Add(gtx.Ops)
						paint.PaintOp{Rect: f32.Rectangle{Max: toPointF(ico.Size())}}.Add(gtx.Ops)
						dims := layout.Dimensions{Size: ico.Size()}
						dims.Size.X += gtx.Px(unit.Dp(4))
						pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
						t.backClick.Add(gtx.Ops)
						return dims
					}),
					layout.Flexed(1, w),
				)


@@ 619,7 623,7 @@ func newSignInPage(env *Env) *signInPage {
			{Header: "IMAP host", Hint: "host:port", Value: &acc.IMAPHost},
			{Header: "SMTP host", Hint: "host:port", Value: &acc.SMTPHost},
		},
		submit: &widget.Button{},
		submit: &widget.Clickable{},
	}
	for _, f := range p.fields {
		f.env = p.env


@@ 634,7 638,7 @@ func newSignInPage(env *Env) *signInPage {
func (p *signInPage) Start(stop <-chan struct{}) {
}

func (p *signInPage) Event(gtx *layout.Context) interface{} {
func (p *signInPage) Event(gtx layout.Context) interface{} {
	if p.submit.Clicked(gtx) {
		for _, f := range p.fields {
			*f.Value = f.edit.Text()


@@ 644,29 648,27 @@ func (p *signInPage) Event(gtx *layout.Context) interface{} {
	return nil
}

func (p *signInPage) Layout(gtx *layout.Context) {
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
func (p *signInPage) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx C) D {
			var t Topbar
			t.Layout(gtx, p.env.insets, func() {
			return t.Layout(gtx, p.env.insets, func(gtx C) D {
				lbl := material.H6(theme, "Sign in")
				lbl.Color = rgb(0xffffff)
				lbl.Layout(gtx)
				return lbl.Layout(gtx)
			})
		}),
		layout.Flexed(1, func() {
			p.layoutSigninForm(gtx)
		}),
		layout.Flexed(1, p.layoutSigninForm),
	)
}

func (p *signInPage) layoutSigninForm(gtx *layout.Context) {
func (p *signInPage) layoutSigninForm(gtx layout.Context) layout.Dimensions {
	l := p.list
	inset := layout.Inset{
		Left:  unit.Max(gtx, unit.Dp(32), p.env.insets.Left),
		Right: unit.Max(gtx, unit.Dp(32), p.env.insets.Right),
	}
	l.Layout(gtx, len(p.fields)+1, func(i int) {
	return l.Layout(gtx, len(p.fields)+1, func(gtx C, i int) D {
		in := inset
		switch {
		case i < len(p.fields):


@@ 674,32 676,27 @@ func (p *signInPage) layoutSigninForm(gtx *layout.Context) {
			if i == 0 {
				in.Top = unit.Dp(32)
			}
			in.Layout(gtx, func() {
				p.fields[i].Layout(gtx)
			})
			return in.Layout(gtx, p.fields[i].Layout)
		default:
			in.Bottom = unit.Max(gtx, unit.Dp(32), p.env.insets.Bottom)
			layout.E.Layout(gtx, func() {
				in.Layout(gtx, func() {
					material.Button(theme, "Sign in").Layout(gtx, p.submit)
				})
			return layout.E.Layout(gtx, func(gtx C) D {
				return in.Layout(gtx, material.Button(theme, p.submit, "Sign in").Layout)
			})
		}
	})
}

func (f *formField) Layout(gtx *layout.Context) {
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
			gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
func (f *formField) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx C) D {
			gtx.Constraints.Min.X = gtx.Constraints.Max.X
			header := material.Caption(theme, f.Header)
			header.Font.Weight = text.Bold
			header.Layout(gtx)
			gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
		}),
		layout.Rigid(func() {
			material.Editor(theme, f.Hint).Layout(gtx, f.edit)
			dims := header.Layout(gtx)
			dims.Size.Y += gtx.Px(unit.Dp(4))
			return dims
		}),
		layout.Rigid(material.Editor(theme, f.edit, f.Hint).Layout),
	)
}



@@ 709,14 706,14 @@ type Background struct {
	Inset  layout.Inset
}

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


@@ 735,6 732,7 @@ func (b *Background) Layout(gtx *layout.Context, w layout.Widget) {
	paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: width, Y: height}}}.Add(gtx.Ops)
	macro.Add()
	stack.Pop()
	return dims
}

func newThreadsPage(env *Env) *threadsPage {


@@ 743,7 741,7 @@ func newThreadsPage(env *Env) *threadsPage {
		list: &layout.List{
			Axis: layout.Vertical,
		},
		fab: new(widget.Button),
		fab: new(widget.Clickable),
	}
}



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

func (p *threadsPage) Event(gtx *layout.Context) interface{} {
func (p *threadsPage) Event(gtx layout.Context) interface{} {
	select {
	case <-p.updates:
		p.fetchThreads()


@@ 795,43 793,41 @@ func (p *threadsPage) fetchThreads() {
	}()
}

func (p *threadsPage) Layout(gtx *layout.Context) {
	layout.Stack{Alignment: layout.SE}.Layout(gtx,
		layout.Stacked(func() {
			layout.Flex{Axis: layout.Vertical}.Layout(gtx,
				layout.Rigid(func() {
func (p *threadsPage) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Stack{Alignment: layout.SE}.Layout(gtx,
		layout.Stacked(func(gtx C) D {
			return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
				layout.Rigid(func(gtx C) D {
					var t Topbar
					t.Layout(gtx, p.env.insets, func() {
					return t.Layout(gtx, p.env.insets, func(gtx C) D {
						lbl := material.H6(theme, p.account.User)
						lbl.Color = rgb(0xffffff)
						lbl.Layout(gtx)
						return lbl.Layout(gtx)
					})
				}),

				layout.Flexed(1, func() {
					p.layoutThreads(gtx)
				}),
				layout.Flexed(1, p.layoutThreads),
			)
		}),
		layout.Stacked(func() {
			layout.SE.Layout(gtx, func() {
				layout.Inset{
		layout.Stacked(func(gtx C) D {
			return layout.SE.Layout(gtx, func(gtx C) D {
				return layout.Inset{
					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() {
					material.IconButton(theme, iconLib.create).Layout(gtx, p.fab)
				})
				}.Layout(gtx,
					material.IconButton(theme, p.fab, iconLib.create).Layout,
				)
			})
		}),
	)
}

func (p *threadsPage) layoutThreads(gtx *layout.Context) {
func (p *threadsPage) layoutThreads(gtx layout.Context) layout.Dimensions {
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(gtx.Ops)
	}
	l.Layout(gtx, len(p.threads), func(i int) {
	return l.Layout(gtx, len(p.threads), func(gtx C, i int) D {
		in := layout.Inset{}
		switch i {
		case 0:


@@ 839,8 835,8 @@ func (p *threadsPage) layoutThreads(gtx *layout.Context) {
		case len(p.threads) - 1:
			in.Bottom = unit.Max(gtx, unit.Dp(4), p.env.insets.Bottom)
		}
		in.Layout(gtx, func() {
			p.thread(gtx, i)
		return in.Layout(gtx, func(gtx C) D {
			return p.thread(gtx, i)
		})
	})
}


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

func (p *threadsPage) thread(gtx *layout.Context, index int) {
func (p *threadsPage) thread(gtx layout.Context, index int) layout.Dimensions {
	t := p.threads[index]
	bgtexcol := rgb(0xbbbbbb)
	fontWeight := text.Normal


@@ 867,25 863,25 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
		Left:  unit.Max(gtx, unit.Dp(16), p.env.insets.Left),
		Right: unit.Max(gtx, unit.Dp(16), p.env.insets.Right),
	}
	in.Layout(gtx, func() {
	return in.Layout(gtx, func(gtx C) D {
		in := layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8)}
		in.Layout(gtx, func() {
			centerRowOpts().Layout(gtx,
				layout.Rigid(func() {
		dims := in.Layout(gtx, func(gtx C) D {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(func(gtx C) D {
					in := layout.Inset{Right: unit.Dp(12)}
					cc := clipCircle{}
					in.Layout(gtx, func() {
						cc.Layout(gtx, func() {
							layout.Stack{Alignment: layout.Center}.Layout(gtx,
					return in.Layout(gtx, func(gtx C) D {
						return cc.Layout(gtx, func(gtx C) D {
							return layout.Stack{Alignment: layout.Center}.Layout(gtx,
								// Background color
								layout.Stacked(func() {
								layout.Stacked(func(gtx C) D {
									sz := image.Point{X: gtx.Px(unit.Dp(48)), Y: gtx.Px(unit.Dp(48))}
									gtx.Constraints = layout.RigidConstraints(gtx.Constraints.Constrain(sz))
									gtx.Constraints = layout.Exact(gtx.Constraints.Constrain(sz))
									color := contactColors[index%len(contactColors)]
									fill{color}.Layout(gtx)
									return fill{color}.Layout(gtx)
								}),
								// Contact initial.
								layout.Stacked(func() {
								layout.Stacked(func(gtx C) D {
									initial := ""
									for _, c := range t.ID {
										initial = string(unicode.ToUpper(c))


@@ 893,50 889,51 @@ func (p *threadsPage) thread(gtx *layout.Context, index int) {
									}
									lbl := material.H5(theme, initial)
									lbl.Color = rgb(0xffffff)
									lbl.Layout(gtx)
									return lbl.Layout(gtx)
								}),
							)
						})
					})
				}),
				layout.Rigid(func() {
					column().Layout(gtx,
						layout.Rigid(func() {
							baseline().Layout(gtx,
								layout.Rigid(func() {
				layout.Rigid(func(gtx C) D {
					return column().Layout(gtx,
						layout.Rigid(func(gtx C) D {
							return baseline().Layout(gtx,
								layout.Rigid(func(gtx C) D {
									lbl := material.H6(theme, t.ID)
									lbl.Font.Weight = fontWeight
									lbl.Layout(gtx)
									return lbl.Layout(gtx)
								}),
								layout.Flexed(1, func() {
									gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
								layout.Flexed(1, func(gtx C) D {
									gtx.Constraints.Min.X = gtx.Constraints.Max.X
									in := layout.Inset{Left: unit.Dp(2)}
									in.Layout(gtx, func() {
									return in.Layout(gtx, func(gtx C) D {
										lbl := material.Caption(theme, formatTime(t.Updated))
										lbl.Color = bgtexcol
										lbl.Alignment = text.End
										lbl.Font.Weight = fontWeight
										lbl.Layout(gtx)
										return lbl.Layout(gtx)
									})
								}),
							)
						}),
						layout.Rigid(func() {
						layout.Rigid(func(gtx C) D {
							in := layout.Inset{Top: unit.Dp(6)}
							in.Layout(gtx, func() {
							return in.Layout(gtx, func(gtx C) D {
								lbl := material.Body2(theme, t.Snippet)
								lbl.Color = bgtexcol
								lbl.Font.Weight = fontWeight
								lbl.MaxLines = 1
								lbl.Layout(gtx)
								return lbl.Layout(gtx)
							})
						}),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		click.Add(gtx.Ops)
		return dims
	})
}



@@ 954,9 951,9 @@ func newThreadPage(env *Env, threadID string) *threadPage {
		msgEdit: &widget.Editor{
			Submit: true,
		},
		send:   new(widget.Button),
		invite: new(widget.Button),
		accept: new(widget.Button),
		send:   new(widget.Clickable),
		invite: new(widget.Clickable),
		accept: new(widget.Clickable),
		topbar: &Topbar{
			Back: true,
		},


@@ 973,7 970,7 @@ func (p *threadPage) Start(stop <-chan struct{}) {
	}()
}

func (p *threadPage) Event(gtx *layout.Context) interface{} {
func (p *threadPage) Event(gtx layout.Context) interface{} {
	select {
	case <-p.updates:
		p.fetchMessages()


@@ 1009,7 1006,7 @@ func (p *threadPage) sendMessage() {
	}
}

func (p *threadPage) Layout(gtx *layout.Context) {
func (p *threadPage) Layout(gtx layout.Context) layout.Dimensions {
	l := p.list
	if l.Dragging() {
		key.HideInputOp{}.Add(gtx.Ops)


@@ 1018,83 1015,82 @@ func (p *threadPage) Layout(gtx *layout.Context) {
	case p.messages = <-p.result:
	default:
	}
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
			p.topbar.Layout(gtx, p.env.insets, func() {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx C) D {
			return p.topbar.Layout(gtx, p.env.insets, func(gtx C) D {
				lbl := material.H6(theme, p.thread.ID)
				lbl.Color = rgb(0xffffff)
				lbl.Layout(gtx)
				return lbl.Layout(gtx)
			})
		}),

		layout.Flexed(1, func() {
			gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
			l.Layout(gtx, len(p.messages), func(i int) {
				p.message(gtx, i)
		layout.Flexed(1, func(gtx C) D {
			gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
			return l.Layout(gtx, len(p.messages), func(gtx C, i int) D {
				return p.message(gtx, i)
			})
		}),

		layout.Rigid(func() {
			in := layout.Inset{
		layout.Rigid(func(gtx C) D {
			return layout.Inset{
				Top:    unit.Dp(16),
				Left:   unit.Max(gtx, unit.Dp(16), p.env.insets.Left),
				Right:  unit.Max(gtx, unit.Dp(16), p.env.insets.Right),
				Bottom: unit.Max(gtx, unit.Dp(16), p.env.insets.Bottom),
			}
			in.Layout(gtx, func() {
			}.Layout(gtx, func(gtx C) D {
				switch {
				case p.thread.PendingInvitation:
					material.Button(theme, "Accept invitation").Layout(gtx, p.accept)
					return material.Button(theme, p.accept, "Accept invitation").Layout(gtx)
				case p.env.client.ContainsSession(p.thread.ID):
					p.layoutMessageBox(gtx)
					return p.layoutMessageBox(gtx)
				default:
					material.Button(theme, "Send invitation").Layout(gtx, p.invite)
					return material.Button(theme, p.invite, "Send invitation").Layout(gtx)
				}
			})
		}),
	)
}

func (p *threadPage) layoutMessageBox(gtx *layout.Context) {
	if mh := gtx.Px(unit.Dp(100)); gtx.Constraints.Height.Max > mh {
		gtx.Constraints.Height.Max = mh
func (p *threadPage) layoutMessageBox(gtx layout.Context) layout.Dimensions {
	if mh := gtx.Px(unit.Dp(100)); gtx.Constraints.Max.Y > mh {
		gtx.Constraints.Max.Y = mh
	}

	var sendHeight int
	layout.Flex{Alignment: layout.End}.Layout(gtx,
		layout.Flexed(1, func() {
			gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
			if gtx.Constraints.Height.Min < sendHeight {
				gtx.Constraints.Height.Min = sendHeight
	return layout.Flex{Alignment: layout.End}.Layout(gtx,
		layout.Flexed(1, func(gtx C) D {
			gtx.Constraints.Min.X = gtx.Constraints.Max.X
			if gtx.Constraints.Min.Y < sendHeight {
				gtx.Constraints.Min.Y = sendHeight
			}
			bg := Background{
				Color:  rgb(0xeeeeee),
				Inset:  layout.Inset{Left: unit.Dp(8), Right: unit.Dp(8)},
				Radius: unit.Dp(10),
			}
			bg.Layout(gtx, func() {
				layout.W.Layout(gtx, func() {
					gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
					ed := material.Editor(theme, "Send a message")
			return bg.Layout(gtx, func(gtx C) D {
				return layout.W.Layout(gtx, func(gtx C) D {
					gtx.Constraints.Min.X = gtx.Constraints.Max.X
					ed := material.Editor(theme, p.msgEdit, "Send a message")
					ed.TextSize = unit.Sp(14)
					ed.Layout(gtx, p.msgEdit)
					return ed.Layout(gtx)
				})
			})
		}),
		layout.Rigid(func() {
			in := layout.Inset{Left: unit.Dp(8)}
			in.Layout(gtx, func() {
				btn := material.IconButton(theme, iconLib.send)
		layout.Rigid(func(gtx C) D {
			return layout.Inset{Left: unit.Dp(8)}.Layout(gtx, func(gtx C) D {
				btn := material.IconButton(theme, p.send, iconLib.send)
				btn.Size = unit.Dp(48)
				btn.Padding = unit.Dp(12)
				btn.Layout(gtx, p.send)
				sendHeight = gtx.Dimensions.Size.Y
				btn.Inset = layout.UniformInset(unit.Dp(12))
				dims := btn.Layout(gtx)
				sendHeight = dims.Size.Y
				return dims
			})
		}),
	)
}

func (p *threadPage) message(gtx *layout.Context, index int) {
func (p *threadPage) message(gtx layout.Context, index int) layout.Dimensions {
	msg := p.messages[index]
	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
	align := layout.W


@@ 1110,53 1106,54 @@ func (p *threadPage) message(gtx *layout.Context, index int) {
	}
	in.Left = unit.Max(gtx, in.Left, p.env.insets.Left)
	in.Right = unit.Max(gtx, in.Right, p.env.insets.Right)
	in.Layout(gtx, func() {
		align.Layout(gtx, func() {
	return in.Layout(gtx, func(gtx C) D {
		return align.Layout(gtx, func(gtx C) D {
			bg := Background{
				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() {
			return bg.Layout(gtx, func(gtx C) D {
				var msgWidth int
				layout.Flex{Axis: layout.Vertical}.Layout(gtx,
					layout.Rigid(func() {
				return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
					layout.Rigid(func(gtx C) D {
						lbl := material.Body2(theme, msg.Message)
						lbl.Color = msgCol
						lbl.Layout(gtx)
						gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
						msgWidth = gtx.Dimensions.Size.X
						dims := lbl.Layout(gtx)
						dims.Size.Y += gtx.Px(unit.Dp(4))
						msgWidth = dims.Size.X
						return dims
					}),

					layout.Rigid(func() {
						gtx.Constraints.Width.Min = msgWidth
					layout.Rigid(func(gtx C) D {
						gtx.Constraints.Min.X = msgWidth
						f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}

						var children []layout.FlexChild
						child := layout.Rigid(func() {
						child := layout.Rigid(func(gtx C) D {
							time := formatTime(msg.Time)
							lbl := material.Caption(theme, time)
							lbl.Color = timecol
							lbl.Layout(gtx)
							return lbl.Layout(gtx)
						})
						children = append(children, child)

						if msg.Own {
							child := layout.Rigid(func() {
							child := layout.Rigid(func(gtx C) D {
								in := layout.Inset{Left: unit.Dp(12)}
								in.Layout(gtx, func() {
								return in.Layout(gtx, func(gtx C) D {
									checkmark := p.checkmark.image(gtx, timecol)
									sz := checkmark.Size()
									if msg.Sent {
										checkmark.Add(gtx.Ops)
										paint.PaintOp{Rect: f32.Rectangle{Max: toPointF(sz)}}.Add(gtx.Ops)
									}
									gtx.Dimensions = layout.Dimensions{Size: sz}
									return layout.Dimensions{Size: sz}
								})
							})
							children = append(children, child)
						}
						f.Layout(gtx, children...)
						return f.Layout(gtx, children...)
					}),
				)
			})


@@ 1193,7 1190,7 @@ func formatTime(t time.Time) string {
	return t.Format(format)
}

func (a *App) update(gtx *layout.Context) {
func (a *App) update(gtx layout.Context) {
	page := a.stack.Current()
	if e := page.Event(gtx); e != nil {
		switch e := e.(type) {


@@ 1216,15 1213,15 @@ type fill struct {
	color color.RGBA
}

func (f fill) Layout(gtx *layout.Context) {
func (f fill) Layout(gtx layout.Context) layout.Dimensions {
	cs := gtx.Constraints
	d := image.Point{X: cs.Width.Min, Y: cs.Height.Min}
	d := cs.Min
	dr := f32.Rectangle{
		Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
	}
	paint.ColorOp{Color: f.color}.Add(gtx.Ops)
	paint.PaintOp{Rect: dr}.Add(gtx.Ops)
	gtx.Dimensions = layout.Dimensions{Size: d, Baseline: d.Y}
	return layout.Dimensions{Size: d, Baseline: d.Y}
}

func column() layout.Flex {


@@ 1242,11 1239,10 @@ func baseline() layout.Flex {
type clipCircle struct {
}

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


@@ 1262,6 1258,7 @@ func (cc *clipCircle) Layout(gtx *layout.Context, w layout.Widget) {
	}.Op(gtx.Ops).Add(gtx.Ops)
	macro.Add()
	stack.Pop()
	return dims
}

func toPointF(p image.Point) f32.Point {

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

require (
	gioui.org v0.0.0-20200503110655-d474b5b16a81
	gioui.org v0.0.0-20200523202849-2451750782b8
	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,6 1,6 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20200503110655-d474b5b16a81 h1:7v3dtTlRk2A7j/iZnuVU8vyTpox1Ddtintk+C/cOW1w=
gioui.org v0.0.0-20200503110655-d474b5b16a81/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04=
gioui.org v0.0.0-20200523202849-2451750782b8 h1:TB+F3jDAVjNemdtgc8yfvsPy1xWkyn7JLOsm5r+YUuQ=
gioui.org v0.0.0-20200523202849-2451750782b8/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04=
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=