~f4814n/frost

d1f5fd3e42f3a6743c0b80c0d05dd58288c5e67d — Fabian Geiselhart 1 year, 3 months ago 1dc06ea
Rework everyting
10 files changed, 532 insertions(+), 888 deletions(-)

D default_page.go
M go.mod
M go.sum
D login_page.go
M main.go
D profile.go
D room_page.go
D ui.go
M util.go
A widgets.go
D default_page.go => default_page.go +0 -293
@@ 1,293 0,0 @@
package main

import (
	"context"
	"fmt"
	"image"
	"image/color"
	"sync"
	"unicode"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/matrix"
	log "github.com/sirupsen/logrus"
)

var contactColors = []color.RGBA{
	{A: 0xff, R: 0xef, G: 0x6c, B: 0x00},
	{A: 0xff, R: 0x00, G: 0x57, B: 0x9b},
	{A: 0xff, R: 0x00, G: 0x97, B: 0xa7},
	{A: 0xff, R: 0x00, G: 0x4d, B: 0x40},
	{A: 0xff, R: 0x7b, G: 0x1f, B: 0xa2},
	{A: 0xff, R: 0x00, G: 0x89, B: 0x7b},
}

type ShowRoomEvent struct {
	Room matrix.Room
}

type DefaultPage struct {
	env *Env

	list   *layout.List
	rooms  []room
	clicks []gesture.Click

	mut sync.Mutex
}

type room struct {
	matrix.Room

	lastMessage matrix.RoomEvent
}

func (r room) preview() string {
	msg := r.lastMessage
	if t, ok := msg.Content["msgtype"].(string); ok {
		switch t {
		case "m.text":
			return fmt.Sprintf("%s: %s", msg.Sender.Displayname(), msg.Content["body"].(string))
		}
	}
	return "nil"
}

func (r room) time() string {
	return r.lastMessage.OriginServerTS.Format("Mon Jan 2 15:04:05")
}

func newDefaultPage(env *Env) *DefaultPage {
	return &DefaultPage{
		env:  env,
		list: &layout.List{Axis: layout.Vertical},
	}
}

func (p *DefaultPage) Start(ctx context.Context) {
	events := make(chan matrix.Event, 10)
	p.env.cli.Notify(events)

	go p.env.cli.SyncForever(ctx, &matrix.SyncOpts{
		Timeout: 3000,
		OnError: func(err error) error {
			log.WithField("error", err).Warning("Sync error")
			return nil
		},
	})

	go func() {
		for {
			select {
			case event := <-events:
				switch event := event.(type) {
				case matrix.StateEvent:
					if event.Type == "m.room.create" {
						p.mut.Lock()
						p.rooms = append(p.rooms, room{Room: event.Room})
						p.clicks = make([]gesture.Click, len(p.rooms))
						p.mut.Unlock()
						p.env.redraw()
					}
				case matrix.RoomEvent:
					if event.Type == "m.room.message" {
						p.mut.Lock()
						for i, room := range p.rooms {
							if room.ID == event.Room.ID {
								room.lastMessage = event
								p.rooms[i] = room
								p.env.redraw()
								break
							}
						}
						p.mut.Unlock()
					}
				}
				p.env.redraw()
			case <-ctx.Done():
				if ctx.Err() != nil {
					log.WithField("error", ctx.Err()).Warn("Sync error. Exiting")
				}
				return
			}
		}
	}()
}

func (p *DefaultPage) Event(gtx *layout.Context) interface{} {
	for i := range p.clicks {
		click := &p.clicks[i]
		for _, e := range click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				room := p.rooms[i]
				return ShowRoomEvent{Room: room.Room}
			}
		}
	}
	return nil
}

func (p *DefaultPage) Layout(gtx *layout.Context) {
	p.mut.Lock()
	defer p.mut.Unlock()

	// layout.Stack{Alignment: layout.SE}.Layout(gtx,
	// 	layout.Stacked(func() {
	// 		layout.Flex{Axis: layout.Vertical}.Layout(gtx,
	// 		    layout.Rigid(func() {
	//
	// 			}),
	// 	}),
	// )
	p.layoutRooms(gtx)
}

func (p *DefaultPage) layoutRooms(gtx *layout.Context) {
	p.list.Layout(gtx, len(p.rooms), func(i int) {
		in := layout.Inset{}
		switch i {
		case 0:
			in.Top = unit.Dp(4)
		case len(p.rooms) - 1:
			in.Bottom = unit.Max(gtx, unit.Dp(4), p.env.inset.Bottom)
		}
		in.Layout(gtx, func() {
			p.layoutRoom(gtx, i)
		})
	})
}

func (p *DefaultPage) layoutRoom(gtx *layout.Context, index int) {
	room := p.rooms[index]
	bgtextcol := rgb(0xbbbbbb)
	fontWeight := text.Normal
	in := layout.Inset{
		Left:  unit.Max(gtx, unit.Dp(16), p.env.inset.Left),
		Right: unit.Max(gtx, unit.Dp(16), p.env.inset.Right),
	}
	in.Layout(gtx, func() {
		in := layout.Inset{Top: unit.Dp(8), Bottom: unit.Dp(8)}
		in.Layout(gtx, func() {
			//centerRowOps
			layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(func() {
					in := layout.Inset{Right: unit.Dp(12)}
					cc := clipCircle{}
					in.Layout(gtx, func() {
						cc.Layout(gtx, func() {
							layout.Stack{Alignment: layout.Center}.Layout(gtx,
								// Background color
								layout.Stacked(func() {
									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)]
									fill{color}.Layout(gtx)
								}),
								// Contact initial.
								layout.Stacked(func() {
									initial := ""
									for _, c := range room.Displayname() {
										initial = string(unicode.ToUpper(c))
										break
									}
									lbl := theme.H5(initial)
									lbl.Color = rgb(0xffffff)
									lbl.Layout(gtx)
								}),
							)
						})
					})
				}),
				layout.Rigid(func() {
					// column()
					layout.Flex{Axis: layout.Vertical}.Layout(gtx,
						layout.Rigid(func() {
							// baseline()
							layout.Flex{Axis: layout.Horizontal, Alignment: layout.Baseline}.Layout(gtx,
								layout.Rigid(func() {
									lbl := theme.H6(room.Displayname())
									lbl.Font.Weight = fontWeight
									lbl.Layout(gtx)
								}),
								layout.Flexed(1, func() {
									gtx.Constraints.Width.Min = gtx.Constraints.Width.Max
									in := layout.Inset{Left: unit.Dp(2)}
									in.Layout(gtx, func() {
										// lbl := theme.Caption(formatTime(t.Updated))
										lbl := theme.Caption(room.time())
										lbl.Color = bgtextcol
										lbl.Alignment = text.End
										lbl.Font.Weight = fontWeight
										lbl.Layout(gtx)
									})
								}),
							)
						}),
						layout.Rigid(func() {
							in := layout.Inset{Top: unit.Dp(6)}
							in.Layout(gtx, func() {
								// lbl := theme.Body2(t.Snippet)
								lbl := theme.Body2(room.preview())
								lbl.Color = bgtextcol
								lbl.Font.Weight = fontWeight
								lbl.MaxLines = 1
								lbl.Layout(gtx)
							})
						}),
					)
				}),
			)
		})
	})
	click := &p.clicks[index]
	pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)
	click.Add(gtx.Ops)
}

type clipCircle struct {
}

func (cc *clipCircle) Layout(gtx *layout.Context, w layout.Widget) {
	var macro op.MacroOp
	macro.Record(gtx.Ops)
	w()
	dims := gtx.Dimensions
	macro.Stop()
	max := dims.Size.X
	if dy := dims.Size.Y; dy > max {
		max = dy
	}
	szf := float32(max)
	rr := szf * .5
	var stack op.StackOp
	stack.Push(gtx.Ops)
	clip.Rect{
		Rect: f32.Rectangle{Max: f32.Point{X: szf, Y: szf}},
		NE:   rr, NW: rr, SE: rr, SW: rr,
	}.Op(gtx.Ops).Add(gtx.Ops)
	macro.Add()
	stack.Pop()
}

type fill struct {
	color color.RGBA
}

func (f fill) Layout(gtx *layout.Context) {
	cs := gtx.Constraints
	d := image.Point{X: cs.Width.Min, Y: cs.Height.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}
}

M go.mod => go.mod +2 -3
@@ 3,14 3,13 @@ module git.sr.ht/~f4814n/frost
go 1.12

require (
	gioui.org v0.0.0-20200429082202-0e70fbc12629
	git.sr.ht/~f4814n/matrix v0.0.0-20200429121240-a6c1f5222720
	gioui.org v0.0.0-20200610092923-bb8bb504d829
	git.sr.ht/~f4814n/matrix v0.0.0-20200610144207-93f7b64a5986
	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
	github.com/kr/pretty v0.2.0 // indirect
	github.com/kr/text v0.2.0 // indirect
	github.com/sirupsen/logrus v1.5.0
	github.com/stretchr/testify v1.5.1 // indirect
	golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
	golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
	gopkg.in/yaml.v2 v2.2.8 // indirect

M go.sum => go.sum +19 -7
@@ 1,26 1,33 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20200429082202-0e70fbc12629 h1:g4d2Tqh6eF5/pJWU+tRNcUI6vKt6vkC5fHsClz3QNxs=
gioui.org v0.0.0-20200429082202-0e70fbc12629/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04=
git.sr.ht/~f4814n/matrix v0.0.0-20200429121240-a6c1f5222720 h1:nyegFRfc2Cz4TRZ3f8VkRNi6yhGNxEG8Ras0dYWj1zw=
git.sr.ht/~f4814n/matrix v0.0.0-20200429121240-a6c1f5222720/go.mod h1:rL4HI7BSnYAlcSPyVF6F0arce1hTmInV0Qsg76eWt8Q=
gioui.org v0.0.0-20200610092923-bb8bb504d829 h1:PXjoSNRvuLtq1svw6Kk0UnurdOdoIeZx0/MvrLD2BOI=
gioui.org v0.0.0-20200610092923-bb8bb504d829/go.mod h1:AHI9rFr6AEEHCb8EPVtb/p5M+NMJRKH58IOp8O3Je04=
git.sr.ht/~f4814n/matrix v0.0.0-20200610144207-93f7b64a5986 h1:74no3Xk4BQOWyepMlwd/9TYKBVVEBBQd/YeF8JrWFUI=
git.sr.ht/~f4814n/matrix v0.0.0-20200610144207-93f7b64a5986/go.mod h1:UGkYwUsyC2TIesV1ex6E6Jt0CUT8RIpfdxpLLW83qxY=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
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=


@@ 31,6 38,7 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDA
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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=


@@ 39,15 47,19 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

D login_page.go => login_page.go +0 -61
@@ 1,61 0,0 @@
package main

import (
	"context"

	"gioui.org/layout"
	"gioui.org/widget"
)

type LoginEvent struct {
	Username string
	Password string
}

type LoginPage struct {
	env *Env
	err error

	list     *layout.List
	username *widget.Editor
	password *widget.Editor

	stay   *widget.CheckBox
	submit *widget.Button
}

func newLoginPage(env *Env) *LoginPage {
	return &LoginPage{
		env:      env,
		list:     &layout.List{Axis: layout.Vertical},
		username: &widget.Editor{SingleLine: true},
		password: &widget.Editor{SingleLine: true},
		stay:     &widget.CheckBox{},
		submit:   &widget.Button{},
	}
}

func (p *LoginPage) Start(ctx context.Context) {
}

func (p *LoginPage) Event(gtx *layout.Context) interface{} {
	if p.submit.Clicked(gtx) {
		return LoginEvent{Username: p.username.Text(), Password: p.password.Text()}
	}
	return nil
}

func (p *LoginPage) Layout(gtx *layout.Context) {
	p.list.Layout(gtx, 4, func(i int) {
		switch i {
		case 0:
			theme.Editor("@example:matrix.org").Layout(gtx, p.username)
		case 1:
			theme.Editor("secret").Layout(gtx, p.password)
		case 2:
			theme.CheckBox("Stay logged in").Layout(gtx, p.stay)
		case 3:
			theme.Button("Log in").Layout(gtx, p.submit)
		}
	})
}

M main.go => main.go +380 -84
@@ 2,132 2,428 @@ package main

import (
	"context"
	"fmt"
	"image"
	"net/http"
	"unicode"

	"gioui.org/app"
	"gioui.org/font/gofont"
	"gioui.org/io/key"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
	memorybackend "git.sr.ht/~f4814n/matrix/backend/memory"
	log "github.com/sirupsen/logrus"
)

var theme *material.Theme

func init() {
	log.SetLevel(log.DebugLevel)
	gofont.Register()
	theme = material.NewTheme()
	theme.Color.Primary = rgb(0x3c98c6)
}

type Env struct {
	cli    *matrix.Client
	inset  layout.Inset
	redraw func()
}
type Gtx = layout.Context
type Dims = layout.Dimensions
type Event interface{}

type Component interface {
	Start(ctx context.Context)
	Event(gtx *layout.Context) interface{}
	Layout(gtx *layout.Context)
type Page interface {
	Start(rx chan Event, tx chan Event)
	Layout(Gtx)
	Stop()
}

type App struct {
	w         *app.Window
	env       Env
	component Component
	events    chan interface{}
	window *app.Window
	client *matrix.Client
	rx, tx chan Event
	page   Page
}

func newApp(w *app.Window) *App {
	a := &App{
		w:      w,
		events: make(chan interface{}, 100),
func newApp() *App {
	return &App{
		rx: make(chan Event, 10),
		tx: make(chan Event, 10),
		client: matrix.NewClient(matrix.ClientOpts{
			HTTPClient: http.DefaultClient,
			Backend:    memorybackend.New(),
		}),
		window: app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		),
	}
	a.env.redraw = a.w.Invalidate
	a.component = newLoadingPage(&a.env)
	return a
}

func (a *App) run() error {
	gtx := layout.NewContext(a.w.Queue())
	for {
		select {
		case e := <-a.w.Events():
		case e := <-a.window.Events():
			switch e := e.(type) {
			case system.DestroyEvent:
				return e.Err
			case system.FrameEvent:
				gtx.Reset(e.Config, e.Size)
				var ops op.Ops
				gtx := layout.NewContext(&ops, e)
				a.Layout(gtx)
				a.env.inset = layout.Inset{
					Top:    e.Insets.Top,
					Left:   e.Insets.Left,
					Right:  e.Insets.Right,
					Bottom: e.Insets.Bottom,
				}
				e.Frame(gtx.Ops)
			case system.StageEvent:
				if e.Stage >= system.StageRunning {
					a.component.Start(context.Background())
				}
			case key.Event:
				a.env.redraw()
				theme = material.NewTheme(gofont.Collection())
				page := NewLoginPage()
				a.page = page
				page.Start(a.tx, a.rx)
			case system.DestroyEvent:
				return e.Err
			default:
				log.Tracef("%#v", e)
			}
			// case app.pageEvents:
		case e := <-a.rx:
			switch e := e.(type) {
			case LoginStartEvent:
				go func() {
					log.Info("Logging in")
					err := a.client.Login(e.Username, e.Password)
					if err != nil {
						log.Info("Failed to log in")
						a.tx <- LoginErrorEvent{Err: err}
						return
					}
					log.Info("Successfully authenticated")
					a.page.Stop()

					a.page = NewOverviewPage(a.client)
					a.page.Start(a.tx, a.rx)
				}()
			case InvalidationEvent:
				a.window.Invalidate()
			default:
				log.Warnf("%#v", e)
			}
		}
	}
}

func (a *App) Layout(gtx *layout.Context) {
	a.update(gtx)
	a.component.Layout(gtx)
}

func (a *App) update(gtx *layout.Context) {
	if e := a.component.Event(gtx); e != nil {
		switch e := e.(type) {
		case StartLoginEvent:
			a.component = newLoginPage(&a.env)
			a.env.redraw()
		case LoginEvent:
			go func() {
				log.Debug("Logging in")
				if err := a.env.cli.Login(e.Username, e.Password); err != nil {
					log.WithField("error", err).Debug("Failed to log in")
					a.component.(*LoginPage).err = err
					a.env.redraw()
				} else {
					log.Debug("Login successful")
					a.component = newDefaultPage(&a.env)
					a.component.Start(context.Background())
				}
				a.env.redraw()
			}()
		case ShowRoomEvent:
			go func() {
				a.component = newRoomPage(&a.env, e.Room)
				a.component.Start(context.Background())
			}()
		case BackEvent:
			a.component = newDefaultPage(&a.env)
			a.component.Start(context.Background())
func (a *App) Layout(gtx Gtx) {
	a.page.Layout(gtx)
}

type InvalidationEvent struct{}

type LoginPage struct {
	rx, tx chan Event

	usernameEditor *widget.Editor
	passwordEditor *widget.Editor
	loginButton    *widget.Clickable

	errorMessage string

	loginInProcess bool
}

type LoginStartEvent struct {
	Username, Password string
}

type LoginErrorEvent struct {
	Err error
}

func NewLoginPage() *LoginPage {
	return &LoginPage{
		loginInProcess: false,
		usernameEditor: &widget.Editor{Submit: true},
		passwordEditor: &widget.Editor{Submit: true},
		loginButton:    new(widget.Clickable),
	}
}

func (l *LoginPage) Start(rx, tx chan Event) {
	l.rx, l.tx = rx, tx
}

func (l *LoginPage) update() {
	if l.loginButton.Clicked() {
		l.loginInProcess = true

		username := l.usernameEditor.Text()
		password := l.passwordEditor.Text()

		l.tx <- LoginStartEvent{Username: username, Password: password}
	}

	for {
		select {
		case e := <-l.rx:
			switch e := e.(type) {
			case LoginErrorEvent:
				l.loginInProcess = false
				l.errorMessage = fmt.Sprintf("%#v", e.Err)
			default:
				log.Warnf("%#v", e)
			}
		default:
			log.Warnf("Unhandled event:%#v", e)
			return
		}
	}
}

func main() {
func (l *LoginPage) Layout(gtx Gtx) {
	l.update()

	if l.loginInProcess {
		gtx = gtx.Disabled()
	}

	layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle, Spacing: layout.SpaceSides}.Layout(gtx,
		layout.Rigid(material.Editor(theme, l.usernameEditor, "username").Layout),
		layout.Rigid(material.Editor(theme, l.passwordEditor, "password").Layout),
		layout.Rigid(material.Caption(theme, l.errorMessage).Layout),
		layout.Rigid(material.Button(theme, l.loginButton, "Login").Layout),
	)
}

func (l *LoginPage) Stop() {
}

type ViewRoomEvent struct {
	Room matrix.Room
}

type OverviewPage struct {
	rx, tx chan Event

	cli    *matrix.Client
	events chan matrix.Event

	roomList    *RoomList
	roomHistory *RoomHistory
}

func NewOverviewPage(cli *matrix.Client) *OverviewPage {
	return &OverviewPage{
		cli:         cli,
		events:      make(chan matrix.Event, 100),
		roomList:    NewRoomList(),
		roomHistory: new(RoomHistory),
	}
}

func (o *OverviewPage) Start(rx, tx chan Event) {
	o.rx, o.tx = rx, tx

	o.cli.Notify(o.events)

	go o.cli.Sync(context.TODO(), &matrix.SyncOpts{
		OnError: func(err error) error {
			log.WithField("error", err).Warn("Sync error")
			return nil
		},
		Timeout: 10000,
	})

	go func() {
		w := app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		)
		c := make(chan matrix.Event, 100)
		for range c {
			o.tx <- InvalidationEvent{}
		}
	}()
}

func (o *OverviewPage) Layout(gtx Gtx) {
	o.update(gtx)

	layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
		layout.Flexed(0.25, o.roomList.Layout),
		layout.Rigid(o.roomHistory.Layout),
	)
}

func (o *OverviewPage) update(gtx Gtx) {
	for _, room := range o.roomList.rooms {
		for _, e := range room.click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				o.roomHistory = NewRoomHistory(room.room)
			}
		}
	}

	for {
		select {
		case event := <-o.events:
			o.roomList.NewEvent(event)
		case event := <-o.rx:
			log.Warnf("%#v", event)
		default:
			return
		}
	}
}

func (o *OverviewPage) Stop() {
}

// RoomList is a widget that displays a list of rooms
type RoomList struct {
	list   *layout.List
	rooms  []roomListElement
}

func NewRoomList() *RoomList {
	return &RoomList{
		list: &layout.List{
			Axis: layout.Vertical,
		},
	}
}

type roomListElement struct {
	click     gesture.Click
	room      matrix.Room
	lastEvent matrix.Event
}

func (w *RoomList) NewEvent(event matrix.Event) {
	var room matrix.Room

	switch event := event.(type) {
	case matrix.RoomEvent:
		room = event.Room
	case matrix.StateEvent:
		room = event.Room
	default:
		return
	}

		if err := newApp(w).run(); err != nil {
	w.rooms = moveToFront(roomListElement{room: room, lastEvent: event}, w.rooms)

	log.Infof("%+v", w.rooms)
}

func moveToFront(needle roomListElement, haystack []roomListElement) []roomListElement {
	if len(haystack) == 0 {
		return []roomListElement{needle}
	}

	if haystack[0].room == needle.room {
		return haystack
	}

	var prev roomListElement
	for i, elem := range haystack {
		switch {
		case i == 0:
			haystack[0] = needle
			prev.room = elem.room
		case elem.room == needle.room:
			haystack[i] = prev
			return haystack
		default:
			haystack[i] = prev
			prev = elem
		}
	}
	return append(haystack, prev)
}

// Layout implements the layout.Widget interface
func (w *RoomList) Layout(gtx Gtx) Dims {
	return w.list.Layout(gtx, len(w.rooms), func(gtx Gtx, index int) Dims {
		return w.rooms[index].Layout(gtx)
	})
}

func (w *roomListElement) Layout(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	fontWeight := text.Normal
	// if w.Unread > 0 {
	// 	bgtextcol = theme.Color.Text
	// 	fontWeight = text.Bold
	// }
	in := layout.Inset{
		Left:  unit.Dp(16),
		Right: unit.Dp(16),
	}

	return in.Layout(gtx, func(gtx Gtx) Dims {
		in := layout.Inset{Top: unit.Dp(16), Bottom: unit.Dp(16)}
		dims := in.Layout(gtx, func(gtx Gtx) Dims {
			return centerRowOpts().Layout(gtx,
				layout.Rigid(func(gtx Gtx) Dims {
					in := layout.Inset{Right: unit.Dp(12)}

					var initial string
					for _, c := range w.room.Displayname() {
						initial = string(unicode.ToUpper(c))
						break
					}

					return in.Layout(gtx, InitialSign{Initial: initial}.Layout)
				}),
				layout.Rigid(func(gtx Gtx) Dims {
					return column().Layout(gtx,
						layout.Rigid(func(gtx Gtx) Dims {
							return baseline().Layout(gtx,
								layout.Rigid(func(gtx Gtx) Dims {
									lbl := material.H6(theme, w.room.Displayname())
									lbl.Font.Weight = fontWeight
									return lbl.Layout(gtx)
								}),
								layout.Flexed(1, func(gtx Gtx) Dims {
									gtx.Constraints.Min.X = gtx.Constraints.Max.X
									in := layout.Inset{Left: unit.Dp(2)}
									return in.Layout(gtx, func(gtx Gtx) Dims {
										lbl := material.Caption(theme, formatTime(eventTime(w.lastEvent)))
										lbl.Color = bgtextcol
										lbl.Alignment = text.End
										lbl.Font.Weight = fontWeight
										return lbl.Layout(gtx)
									})
								}),
							)
						}),
						layout.Rigid(func(gtx Gtx) Dims {
							in := layout.Inset{Top: unit.Dp(6)}
							return in.Layout(gtx, func(gtx Gtx) Dims {
								lbl := material.Body2(theme, fmt.Sprint(w.lastEvent))
								lbl.Color = bgtextcol
								lbl.Font.Weight = fontWeight
								lbl.MaxLines = 1
								return lbl.Layout(gtx)
							})
						}),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.click.Add(gtx.Ops)
		return dims
	})
}

// RoomHistory is a widget that displays the chat history of a room
type RoomHistory struct {
	room matrix.Room
}

func NewRoomHistory(room matrix.Room) *RoomHistory {
	return &RoomHistory{
		room: room,
	}
}

// Layout implements th layout.Widget interface
func (w *RoomHistory) Layout(gtx Gtx) Dims {
	return Dims{}
}

func main() {
	log.SetLevel(log.DebugLevel)
	go func() {
		a := newApp()
		if err := a.run(); err != nil {
			log.Fatal(err)
		}
	}()

D profile.go => profile.go +0 -12
@@ 1,12 0,0 @@
//build +profile

package main

import (
	"net/http"
	_ "net/http/pprof"
)

func init() {
	go http.ListenAndServe(":8181", nil)
}

D room_page.go => room_page.go +0 -290
@@ 1,290 0,0 @@
package main

import (
	"context"
	"fmt"
	log "github.com/sirupsen/logrus"
	"image/color"
	"time"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget"
	"git.sr.ht/~f4814n/matrix"
)

type roomPage struct {
	env    *Env
	room   matrix.Room
	notify chan matrix.Event
	list   *layout.List
	events []matrix.Event
	topbar *Topbar

	msgEdit *widget.Editor
	send    *widget.Button
}

func newRoomPage(env *Env, room matrix.Room) *roomPage {
	return &roomPage{
		env:    env,
		room:   room,
		notify: make(chan matrix.Event),
		msgEdit: &widget.Editor{
			Submit: true,
		},
		send: new(widget.Button),
		list: &layout.List{
			Axis:        layout.Vertical,
			ScrollToEnd: true,
		},
		topbar: &Topbar{
			Back: true,
		},
	}
}

func (p *roomPage) Start(ctx context.Context) {
	p.room.Notify(p.notify)
	go func() {
		history, err := p.room.History()
		fmt.Printf("%#v\n", err)
		for history.Next() {
			for i, j := 0, len(history.Events)-1; i < j; i, j = i+1, j-1 {
				history.Events[i], history.Events[j] = history.Events[j], history.Events[i]
			}
			p.events = append(history.Events, p.events...)
		}
	}()

	go func() {
		for e := range p.notify {
			p.events = append(p.events, e)
		}
	}()
}

func (p *roomPage) Event(gtx *layout.Context) interface{} {
	for _, e := range p.msgEdit.Events(gtx) {
		if _, ok := e.(widget.SubmitEvent); ok {
			go p.sendEvent()
		}
	}
	if p.send.Clicked(gtx) {
		go p.sendEvent()
	}
	return p.topbar.Event(gtx)
}

func (p *roomPage) sendEvent() {
	if t := p.msgEdit.Text(); t != "" {
		_, err := p.room.SendRoomEvent("m.room.message", map[string]interface{}{"body": t, "msgtype": "m.text"})
		if err != nil {
			log.WithField("error", err).Warn("Failed to send event")
		}
		p.msgEdit.SetText("")
	}
}

func (p *roomPage) Layout(gtx *layout.Context) {
	layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func() {
			p.topbar.Layout(gtx, p.env.inset, func() {
				lbl := theme.H6(p.room.Displayname())
				lbl.Color = rgb(0xffffff)
				lbl.Layout(gtx)
			})
		}),
		layout.Flexed(1, func() {
			gtx.Constraints.Height.Min = gtx.Constraints.Height.Max
			p.list.Layout(gtx, len(p.events), func(i int) {
				p.layoutEvent(gtx, i)
			})
		}),

		layout.Rigid(func() {
			in := layout.Inset{
				Top:    unit.Dp(16),
				Left:   unit.Max(gtx, unit.Dp(16), p.env.inset.Left),
				Right:  unit.Max(gtx, unit.Dp(16), p.env.inset.Right),
				Bottom: unit.Max(gtx, unit.Dp(16), p.env.inset.Bottom),
			}
			in.Layout(gtx, func() {
				p.layoutMessageBox(gtx)
			})
		}),
	)
}

func (p *roomPage) layoutMessageBox(gtx *layout.Context) {
	if mh := gtx.Px(unit.Dp(100)); gtx.Constraints.Height.Max > mh {
		gtx.Constraints.Height.Max = 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
			}
			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 := theme.Editor("Send a message")
					ed.TextSize = unit.Sp(14)
					ed.Layout(gtx, p.msgEdit)
				})
			})
		}),
		layout.Rigid(func() {
			in := layout.Inset{Left: unit.Dp(8)}
			in.Layout(gtx, func() {
				btn := theme.Button("->")
				// btn.Size = unit.Dp(48)
				// btn.Padding = unit.Dp(12)
				btn.Layout(gtx, p.send)
				sendHeight = gtx.Dimensions.Size.Y
			})
		}),
	)

}

func (p *roomPage) layoutEvent(gtx *layout.Context, index int) {
	event := p.events[index]
	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
	align := layout.W
	msgCol := rgb(0xffffff)
	bgcol := theme.Color.Primary
	timecol := argb(0xaaaaaaaa)

	if event, ok := event.(matrix.RoomEvent); ok {
		if event.Sender.User.ID == p.env.cli.User().ID {
			in.Left, in.Right = in.Right, in.Left
			align = layout.E
			bgcol = rgb(0xeeeeee)
			msgCol = theme.Color.Text
			timecol = rgb(0x888888)
		}

		// TODO: State events

		in.Left = unit.Max(gtx, in.Left, p.env.inset.Left)
		in.Right = unit.Max(gtx, in.Right, p.env.inset.Right)

		in.Layout(gtx, func() {
			align.Layout(gtx, func() {
				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() {
					var msgWidth int
					layout.Flex{Axis: layout.Vertical}.Layout(gtx,
						layout.Rigid(func() {
							lbl := theme.Body2(formatEvent(event))
							lbl.Color = msgCol
							lbl.Layout(gtx)
							gtx.Dimensions.Size.Y += gtx.Px(unit.Dp(4))
							msgWidth = gtx.Dimensions.Size.X
						}),
						layout.Rigid(func() {
							gtx.Constraints.Width.Min = msgWidth
							f := layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween, Alignment: layout.Middle}

							var children []layout.FlexChild
							child := layout.Rigid(func() {
								time := formatTime(event.OriginServerTS)
								lbl := theme.Caption(time)
								lbl.Color = timecol
								lbl.Layout(gtx)
							})
							children = append(children, child)

							// TODO Checkmark
							// if msg.Own {
							// 	child = layout.Rigid(func() {
							// 		in := layout.Inset{Left: unit.Dp(12)}
							// 		in.Layout(gtx, func() {
							// 			checkmark := p.checkmark
							// 		})
							// 	})
							// }
							f.Layout(gtx, children...)
						}),
					)
				})
			})
		})
	}
}

type Background struct {
	Color  color.RGBA
	Radius unit.Value
	Inset  layout.Inset
}

func (b *Background) Layout(gtx *layout.Context, w layout.Widget) {
	var macro op.MacroOp
	macro.Record(gtx.Ops)
	b.Inset.Layout(gtx, w)
	macro.Stop()
	var stack op.StackOp
	stack.Push(gtx.Ops)
	size := gtx.Dimensions.Size
	width, height := float32(size.X), float32(size.Y)
	if r := float32(gtx.Px(b.Radius)); r > 0 {
		if r > width/2 {
			r = width / 2
		}
		if r > height/2 {
			r = height / 2
		}
		clip.Rect{
			Rect: f32.Rectangle{Max: f32.Point{
				X: width, Y: height,
			}}, NW: r, NE: r, SW: r, SE: r,
		}.Op(gtx.Ops).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()
	stack.Pop()
}

func formatTime(t time.Time) string {
	y, m, d := t.Date()
	tday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	y, m, d = time.Now().Date()
	nday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	n := int(nday.Sub(tday) / (time.Hour * 24))
	format := "Jan _2 15:04"
	if n < 7 {
		format = "Mon 15:04"
	}
	return t.Format(format)
}

func formatEvent(e matrix.Event) string {
	switch e := e.(type) {
	case matrix.RoomEvent:
		if e.Type == "m.room.message" {
			return e.Content["body"].(string)
		}
		return fmt.Sprintf("%#v", e)
	default:
		return fmt.Sprintf("%#v", e)
	}
}

D ui.go => ui.go +0 -137
@@ 1,137 0,0 @@
package main

import (
	"context"
	"image"
	"image/color"
	"image/draw"
	"net/http"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"git.sr.ht/~f4814n/matrix"
	"golang.org/x/exp/shiny/iconvg"
	"golang.org/x/exp/shiny/materialdesign/icons"

	membackend "git.sr.ht/~f4814n/matrix/backend/memory"
)

type StartLoginEvent struct{}

type AuthenticatedEvent struct{}

type LoadingPage struct {
	env   *Env
	event interface{}
}

func newLoadingPage(env *Env) *LoadingPage {
	return &LoadingPage{
		env: env,
	}
}

func (p *LoadingPage) Start(ctx context.Context) {
	p.env.cli = matrix.NewClient(matrix.ClientOpts{
		HTTPClient: http.DefaultClient,
		Backend:    membackend.New(),
	})

	if p.env.cli.User() == nil {
		p.event = StartLoginEvent{}
	} else {
		p.event = AuthenticatedEvent{}
	}
}

func (p *LoadingPage) Event(gtx *layout.Context) interface{} {
	return p.event
}

func (p *LoadingPage) Layout(gtx *layout.Context) {
}

type Topbar struct {
	Back bool

	backClick gesture.Click
}

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

type BackEvent struct{}

func (t *Topbar) Layout(gtx *layout.Context, insets layout.Inset, w layout.Widget) {
	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)
						}
					}),
					layout.Flexed(1, w),
				)
			})
		}),
	)
}

func toPointF(p image.Point) f32.Point {
	return f32.Point{X: float32(p.X), Y: float32(p.Y)}
}

type icon struct {
	src  []byte
	size unit.Value

	// Cached values.
	op      paint.ImageOp
	imgSize int
}

func (ic *icon) image(c unit.Converter, col color.RGBA) paint.ImageOp {
	sz := c.Px(ic.size)
	if sz == ic.imgSize {
		return ic.op
	}
	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)
	m.Palette[0] = col
	iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{
		Palette: &m.Palette,
	})
	ic.op = paint.NewImageOp(img)
	ic.imgSize = sz
	return ic.op
}

M util.go => util.go +70 -1
@@ 1,6 1,14 @@
package main

import "image/color"
import (
	"image/color"
	"time"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op/paint"
	"git.sr.ht/~f4814n/matrix"
)

func rgb(c uint32) color.RGBA {
	return argb((0xff << 24) | c)


@@ 9,3 17,64 @@ func rgb(c uint32) color.RGBA {
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 centerRowOpts() layout.Flex {
	return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}
}

var contactColors = []color.RGBA{
	{A: 0xff, R: 0xef, G: 0x6c, B: 0x00},
	{A: 0xff, R: 0x00, G: 0x57, B: 0x9b},
	{A: 0xff, R: 0x00, G: 0x97, B: 0xa7},
	{A: 0xff, R: 0x00, G: 0x4d, B: 0x40},
	{A: 0xff, R: 0x7b, G: 0x1f, B: 0xa2},
	{A: 0xff, R: 0x00, G: 0x89, B: 0x7b},
}

func column() layout.Flex {
	return layout.Flex{Axis: layout.Vertical}
}

func baseline() layout.Flex {
	return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Baseline}
}

// formatTime formats a time relative to now. For times within a
// week the date is left out.
func formatTime(t time.Time) string {
	y, m, d := t.Date()
	tday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	y, m, d = time.Now().Date()
	nday := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	n := int(nday.Sub(tday) / (time.Hour * 24))
	format := "Jan _2 15:04"
	if n < 7 {
		format = "Mon 15:04"
	}
	return t.Format(format)
}

type fill struct {
	color color.RGBA
}

func (f fill) Layout(gtx layout.Context) layout.Dimensions {
	cs := gtx.Constraints
	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)
	return layout.Dimensions{Size: d, Baseline: d.Y}
}

func eventTime(event matrix.Event) time.Time {
	switch event := event.(type) {
	case matrix.RoomEvent:
		return event.OriginServerTS
	case matrix.StateEvent:
		return event.OriginServerTS
	}
	return time.Time{}
}

A widgets.go => widgets.go +61 -0
@@ 0,0 1,61 @@
package main

import (
	"image"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/unit"
	"gioui.org/widget/material"
)

// InitialSign is a widget which shows a Initial on a colored background
type InitialSign struct {
	Initial string
}

func (w InitialSign) Layout(gtx Gtx) Dims {
	cc := clipCircle{}
	return cc.Layout(gtx, func(gtx Gtx) Dims {
		return layout.Stack{Alignment: layout.Center}.Layout(gtx,
			// Background Color
			layout.Stacked(func(gtx Gtx) Dims {
				sz := image.Point{X: gtx.Px(unit.Dp(48)), Y: gtx.Px(unit.Dp(48))}
				gtx.Constraints = layout.Exact(gtx.Constraints.Constrain(sz))
				color := contactColors[w.Initial[0]%6]
				return fill{color}.Layout(gtx)
			}),
			// Initial
			layout.Stacked(func(gtx Gtx) Dims {
				lbl := material.H5(theme, w.Initial)
				lbl.Color = rgb(0xffffff)
				return lbl.Layout(gtx)
			}),
		)
	})
}

type clipCircle struct {
}

func (cc *clipCircle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
	macro := op.Record(gtx.Ops)
	dims := w(gtx)
	macroCall := macro.Stop()
	max := dims.Size.X
	if dy := dims.Size.Y; dy > max {
		max = dy
	}
	szf := float32(max)
	rr := szf * .5
	stack := op.Push(gtx.Ops)
	clip.Rect{
		Rect: f32.Rectangle{Max: f32.Point{X: szf, Y: szf}},
		NE:   rr, NW: rr, SE: rr, SW: rr,
	}.Op(gtx.Ops).Add(gtx.Ops)
	macroCall.Add(gtx.Ops)
	stack.Pop()
	return dims
}