~f4814n/frost

c582ddc68ee8dfc33ad392899d94d6a493b88dd7 — Fabian Geiselhart 4 months ago fb80ddb
Start using materials
M cmd/frost/main.go => cmd/frost/main.go +16 -9
@@ 19,6 19,7 @@ import (
	"git.sr.ht/~f4814n/frost/view/roomhistory"
	"git.sr.ht/~f4814n/frost/view/roomlist"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"go.uber.org/zap"
)



@@ 37,11 38,12 @@ type App struct {
	platform platform.Platform
	logger   *zap.Logger
	cache    frost.Component

	mux *frost.Mux
	mux      *frost.Mux

	full, left, middle, right         frost.View
	fullID, leftID, middleID, rightID uint

	modalLayer *materials.ModalLayer
}

func newApp() *App {


@@ 64,8 66,9 @@ func newApp() *App {
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		),
		platform: platform,
		logger:   logger.Named("frost"),
		platform:   platform,
		logger:     logger.Named("frost"),
		modalLayer: materials.NewModal(),
	}

	a.cache = cache.New(a.cli, platformCache, logger.Named("cache"))


@@ 109,7 112,10 @@ func (a *App) run() error {
					Bottom: e.Insets.Bottom,
					Left:   e.Insets.Left,
					Right:  e.Insets.Right,
				}.Layout(gtx, a.Layout)
				}.Layout(gtx, func(gtx Gtx) Dims {
					a.Layout(gtx)
					return a.modalLayer.Layout(gtx, theme)
				})
				e.Frame(gtx.Ops)
			case *system.CommandEvent:
				switch e.Type {


@@ 135,7 141,7 @@ func (a *App) run() error {
				}

				self, _ := a.cli.User()
				a.middle = roomhistory.New(self, e.Room, theme, a.logger.Named("roomhistory"))
				a.middle = roomhistory.New(self, e.Room, a.modalLayer, theme, a.logger.Named("roomhistory"))

				id, rx, tx := a.mux.Register()
				a.middleID = id


@@ 147,7 153,7 @@ func (a *App) run() error {
					a.right = nil
				}

				a.right = memberlist.New(e.Room, theme, a.logger.Named("memberlist"))
				a.right = memberlist.New(e.Room, a.modalLayer, theme, a.logger.Named("memberlist"))
				id, rx, tx := a.mux.Register()
				a.rightID = id
				go a.right.Run(rx, tx)


@@ 255,8 261,9 @@ func (a *App) login(event login.StartEvent) {

	a.full.Stop()
	a.mux.Unregister(a.fullID)
	a.full = nil

	a.left = roomlist.New(a.cli, theme, a.logger.Named("roomlist"))
	a.left = roomlist.New(a.cli, a.modalLayer, theme, a.logger.Named("roomlist"))
	id, rx, tx := a.mux.Register()
	a.leftID = id
	go a.left.Run(rx, tx)


@@ 287,7 294,7 @@ func (a *App) initialView() {
		}
		a.sync()

		a.left = roomlist.New(a.cli, theme, a.logger.Named("roomlist"))
		a.left = roomlist.New(a.cli, a.modalLayer, theme, a.logger.Named("roomlist"))
		id, rx, tx := a.mux.Register()
		a.leftID = id
		go a.left.Run(rx, tx)

M go.mod => go.mod +3 -3
@@ 6,12 6,12 @@ require (
	gioui.org v0.0.0-20201224113856-0416fffc096b
	gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a
	git.sr.ht/~f4814n/matrix v0.0.0-20201218143803-770e10bd6252
	git.sr.ht/~whereswaldon/materials v0.0.0-20201220015048-3f4fb49d1fd9
	go.uber.org/zap v1.16.0
	golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7
	golang.org/x/image v0.0.0-20200618115811-c13761719519
	golang.org/x/exp v0.0.0-20201203231725-fa01524bc59d
	golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
	golang.org/x/mod v0.4.0 // indirect
	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
	golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect
	golang.org/x/tools v0.0.0-20201215192005-fa10ef0b8743 // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	gopkg.in/yaml.v2 v2.2.4 // indirect

M go.sum => go.sum +11 -4
@@ 1,11 1,14 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20200726090130-3b95e2918359/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU=
gioui.org v0.0.0-20201219144351-00c4a53036ad/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org v0.0.0-20201224113856-0416fffc096b h1:S5tOnnkKUhmtF9cuCtD4IAVPXPItqpPIUQBISIQ60Uo=
gioui.org v0.0.0-20201224113856-0416fffc096b/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a h1:BPgJqeQSxuX6CzFQ2EGDj4Kr2ZzwiqwdLyAYLXgxR+k=
gioui.org/cmd v0.0.0-20201020094634-d5bdf0756a5a/go.mod h1:dlmJnCEkOpRaChYxRmJZ5S4jk6y7DCfWnec39xGbUYk=
git.sr.ht/~f4814n/matrix v0.0.0-20201218143803-770e10bd6252 h1:82PU+gyoqJQWqdCZ5aefQ0YtIbcTlpCGQ1366UEChpc=
git.sr.ht/~f4814n/matrix v0.0.0-20201218143803-770e10bd6252/go.mod h1:yR3YNkJR8S3qLkGh178oKRm3MnsT5cERNEXOF8eFOIo=
git.sr.ht/~whereswaldon/materials v0.0.0-20201220015048-3f4fb49d1fd9 h1:rb+UW4XloZFJvW022LJ++VyDRilIAcgEJbBLX3xtXd4=
git.sr.ht/~whereswaldon/materials v0.0.0-20201220015048-3f4fb49d1fd9/go.mod h1:evzscth58GGa3fzjGHmL2aR//0YLi52UfgfyTGFpMCM=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=


@@ 77,12 80,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7 h1:2/QncOxxpPAdiH+E00abYw/SaQG353gltz79Nl1zrYE=
golang.org/x/exp v0.0.0-20201008143054-e3b2a7f2fdc7/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/exp v0.0.0-20201203231725-fa01524bc59d h1:FscZqdyN/qhN9in1p2FLXl6vsrWY792O5bak6GHqVs0=
golang.org/x/exp v0.0.0-20201203231725-fa01524bc59d/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=


@@ 116,14 121,16 @@ golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 h1:Qo9oJ566/Sq7N4hrGftVXs8GI2CXBCuOd4S2wHE/e0M=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=

M view/login/view.go => view/login/view.go +35 -10
@@ 5,6 5,7 @@ import (

	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/util"
	"git.sr.ht/~whereswaldon/materials"

	"go.uber.org/zap"



@@ 24,8 25,8 @@ type view struct {

	theme *material.Theme

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

	errorMessage string


@@ 38,8 39,8 @@ type view struct {
func New(theme *material.Theme, logger *zap.Logger) frost.View {
	return &view{
		loginInProcess: false,
		usernameEditor: &widget.Editor{Submit: true, SingleLine: true},
		passwordEditor: &widget.Editor{Submit: true, SingleLine: true, Mask: '•'},
		usernameEditor: &materials.TextField{Editor: widget.Editor{Submit: true, SingleLine: true}},
		passwordEditor: &materials.TextField{Editor: widget.Editor{Submit: true, SingleLine: true, Mask: '•'}},
		loginButton:    new(widget.Clickable),
		logger:         logger,
		theme:          theme,


@@ 48,16 49,27 @@ func New(theme *material.Theme, logger *zap.Logger) frost.View {

func (l *view) Run(rx, tx chan frost.Event) {
	l.rx, l.tx = rx, tx

	l.usernameEditor.Focus()
}

func (l *view) update() {
	if l.loginButton.Clicked() {
		l.loginInProcess = true
		l.startLogin()
	}

		username := l.usernameEditor.Text()
		password := l.passwordEditor.Text()
	for _, event := range l.usernameEditor.Events() {
		switch event.(type) {
		case widget.SubmitEvent:
			l.passwordEditor.Focus()
		}
	}

		l.tx <- StartEvent{Username: username, Password: password}
	for _, event := range l.passwordEditor.Events() {
		switch event.(type) {
		case widget.SubmitEvent:
			l.startLogin()
		}
	}

	for {


@@ 74,6 86,15 @@ func (l *view) update() {
	}
}

func (l *view) startLogin() {
	l.loginInProcess = true

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

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

func (l *view) Layout(gtx layout.Context) layout.Dimensions {
	l.update()



@@ 84,8 105,12 @@ func (l *view) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Center.Layout(gtx, func(gtx g) d {
		gtx.Constraints = layout.Exact(image.Point{X: gtx.Px(unit.Dp(280)), Y: gtx.Px(unit.Dp(480))})
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(material.Editor(l.theme, l.usernameEditor, "Username").Layout),
			layout.Rigid(material.Editor(l.theme, l.passwordEditor, "Password").Layout),
			layout.Rigid(func(gtx g) d {
				return l.usernameEditor.Layout(gtx, l.theme, "Username")
			}),
			layout.Rigid(func(gtx g) d {
				return l.passwordEditor.Layout(gtx, l.theme, "Password")
			}),
			layout.Rigid(func(gtx g) d {
				in := layout.Inset{}


M view/memberlist/memberlist.go => view/memberlist/memberlist.go +16 -27
@@ 10,13 10,13 @@ import (

	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/component/cache"
	"git.sr.ht/~f4814n/frost/util"
	fwidget "git.sr.ht/~f4814n/frost/widget"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"go.uber.org/zap"
	"golang.org/x/exp/shiny/materialdesign/icons"
	_ "golang.org/x/image/bmp"


@@ 44,8 44,6 @@ type view struct {

	matrixEvents chan matrix.Event

	backButton *widget.Clickable

	userList *layout.List
	users    []matrix.Member



@@ 56,17 54,23 @@ type view struct {
	theme *material.Theme

	logger *zap.Logger

	appBar *materials.AppBar
}

func New(room matrix.Room, theme *material.Theme, logger *zap.Logger) frost.View {
func New(room matrix.Room, modalLayer *materials.ModalLayer, theme *material.Theme, logger *zap.Logger) frost.View {
	appBar := materials.NewAppBar(modalLayer)
	appBar.NavigationIcon = util.MustIcon(icons.NavigationArrowBack)
	appBar.Title = "Room members"

	return &view{
		matrixEvents: make(chan matrix.Event, 100),
		avatars:      make(map[string]image.Image),
		backButton:   new(widget.Clickable),
		userList:     &layout.List{Axis: layout.Vertical},
		room:         room,
		logger:       logger,
		theme:        theme,
		appBar:       appBar,
	}
}



@@ 156,6 160,12 @@ func (v *view) updateAvatar(user matrix.Member) {
}

func (v *view) update(gtx g) {
	for _, event := range v.appBar.Events(gtx) {
		switch event.(type) {
		case materials.AppBarNavigationClicked:
			v.tx <- frost.CloseMemberlistEvent{}
		}
	}
loop:
	for {
		select {


@@ 177,17 187,13 @@ loop:
			break loop
		}
	}

	if v.backButton.Clicked() {
		v.tx <- frost.CloseMemberlistEvent{}
	}
}

func (v *view) Layout(gtx layout.Context) layout.Dimensions {
	v.update(gtx)

	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(v.topbar),
		layout.Rigid(fwidget.LayoutTheme(v.appBar.Layout, v.theme)),
		layout.Flexed(1, func(gtx g) d {
			return v.userList.Layout(gtx, len(v.users), v.listElem)
		}),


@@ 224,20 230,3 @@ func (v *view) listElem(gtx g, index int) d {
func (v *view) Stop() {
	v.room.Stop(v.matrixEvents)
}

func (v *view) topbar(gtx g) d {
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(func(gtx g) d {
			gtx.Constraints.Min.X = gtx.Constraints.Max.X
			return fwidget.Fill{Color: v.theme.Palette.ContrastBg}.Layout(gtx)
		}),
		layout.Stacked(func(gtx g) d {
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(func(gtx g) d {
					btn := material.IconButton(v.theme, v.backButton, util.MustIcon(icons.NavigationArrowBack))
					return btn.Layout(gtx)
				}),
			)
		}),
	)
}

A view/roomhistory/composer.go => view/roomhistory/composer.go +92 -0
@@ 0,0 1,92 @@
package roomhistory

import (
	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost/util"
	"git.sr.ht/~whereswaldon/materials"
	"golang.org/x/exp/shiny/materialdesign/icons"
)

type messageComposer struct {
	editor     *materials.TextField
	submit     *widget.Clickable
	submitIcon *widget.Icon

	disabled  bool
	sendError error
}

func newMessageComposer() *messageComposer {
	return &messageComposer{
		editor:     &materials.TextField{},
		submit:     new(widget.Clickable),
		submitIcon: util.MustIcon(icons.ContentSend),
		disabled:   false,
	}
}

func (m *messageComposer) Layout(gtx g, theme *material.Theme) d {
	if mh := gtx.Px(unit.Dp(200)); gtx.Constraints.Max.Y > mh {
		gtx.Constraints.Max.Y = mh
	}

	if m.disabled {
		gtx = gtx.Disabled()
	}

	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
		layout.Flexed(1, func(gtx g) d {
			return m.layoutEditor(gtx, theme)
		}),
		layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
		layout.Rigid(func(gtx g) d {
			btn := material.IconButton(theme, m.submit, m.submitIcon)
			btn.Size = unit.Dp(24)
			btn.Inset = layout.UniformInset(unit.Dp(6))

			return btn.Layout(gtx)
		}),
	)
}

func (m *messageComposer) layoutEditor(gtx g, theme *material.Theme) d {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx g) d {
			if m.sendError != nil {
				lbl := material.Body2(theme, m.sendError.Error())
				lbl.Color = util.NRGB(0xff0000)
				return lbl.Layout(gtx)
			}
			return d{}
		}),
		layout.Rigid(func(gtx g) d {
			return layout.W.Layout(gtx, func(gtx g) d {
				gtx.Constraints.Min.X = gtx.Constraints.Max.X
				return m.editor.Layout(gtx, theme, "Enter message")
			})
		}),
	)
}

func (m *messageComposer) Submitted() bool {
	return m.submit.Clicked()
}

func (m *messageComposer) Text() string {
	return m.editor.Text()
}

func (m *messageComposer) Disable() {
	m.disabled = true
}

// Reenable activates the composer after a event has been sent. If the error
// is not nil, then the editor is cleared
func (m *messageComposer) Reenable(err error) {
	m.disabled = false
	m.sendError = err
	m.editor.SetText("")
}

A view/roomhistory/event.go => view/roomhistory/event.go +226 -0
@@ 0,0 1,226 @@
package roomhistory

import (
	"fmt"
	"time"

	fwidget "git.sr.ht/~f4814n/frost/widget"

	"git.sr.ht/~f4814n/frost/util"

	"gioui.org/layout"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
)

type statusEvent struct {
	Text    string
	Invalid bool
}

func newStatusEvent(ev matrix.StateEvent) statusEvent {
	senderName := ev.Sender.Displayname()

	var (
		text    string
		invalid bool
	)

	switch ev.Type {
	case "m.room.member":
		text = fmt.Sprintf("%s changed their profile", ev.StateKey)
	case "m.room.create":
		text = fmt.Sprintf("%s created the room", senderName)
	case "m.room.power_levels":
		text = fmt.Sprintf("Room power levels were changed by %s", senderName)
	case "m.room.join_rules":
		text = fmt.Sprintf("Join rules were changed by %s", senderName)
	case "m.room.name":
		text = fmt.Sprintf("%s changed the room name to \"%s\"", senderName, ev.Room.Displayname())
	case "m.room.topic":
		var str string
		if topic, ok := ev.Content["topic"]; ok {
			str = fmt.Sprintf("%s changed the room topic to \"%s\"", senderName, topic)
		} else {
			str = fmt.Sprintf("%s removed the room topic", senderName)
		}
		text = str
	case "m.room.guest_access":
		var str string
		if access, ok := ev.Content["guest_access"]; ok && access == "can_join" || access == "forbidden" {
			if access == "can_join" {
				str = fmt.Sprintf("%s allowed guest to join", senderName)
			} else {
				str = fmt.Sprintf("%s disallowed guests to join", senderName)
			}
		} else {
			str = "[Unformattable Event]"
			invalid = true
		}
		text = str
	case "m.room.history_visibility":
		var str string
		if access, ok := ev.Content["history_visibility"]; ok {
			str = fmt.Sprintf("%s set the history visibility to \"%s\"", senderName, access)
		} else {
			str = "[Unformattable Event]"
			invalid = true
		}
		text = str
	case "m.room.canonical_alias":
		var str string
		if alias, ok := ev.Content["canonical_alias"]; ok {
			str = fmt.Sprintf("%s changed the canonical alias to \"%s\"", senderName, alias)
		} else {
			str = fmt.Sprintf("%s set no or removed the canonical alias", senderName)
		}
		text = str
	default:
		invalid = true
		text = "[Unformattable Event]"
	}

	return statusEvent{
		Text:    text,
		Invalid: invalid,
	}
}

func (e statusEvent) Layout(gtx g, theme *material.Theme) d {
	align := layout.Center
	bgcol := util.NRGB(0xeeeeee)

	return align.Layout(gtx, func(gtx g) d {
		bg := fwidget.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),
		}

		return bg.Layout(gtx, func(gtx g) d {
			lbl := material.Body1(theme, e.Text)
			if e.Invalid {
				lbl.Color = util.NRGB(0xff0000)
			} else {
				lbl.Color = theme.Palette.ContrastBg
			}
			return lbl.Layout(gtx)
		})
	})
}

type messageEvent struct {
	Time    time.Time
	Text    string
	Sender  matrix.Member
	Own     bool
	Invalid bool
}

func newMessageEvent(own bool, matrixEvent matrix.RoomEvent) messageEvent {
	event := messageEvent{
		Time:   matrixEvent.OriginServerTS,
		Sender: matrixEvent.Sender,
		Own:    own,
	}

	if matrixEvent.Type == "m.room.encrypted" {
		var err error
		matrixEvent, err = matrixEvent.Decrypted()
		if err != nil {
			event.Text = err.Error()
			event.Invalid = true
		}
	}

	if body, ok := matrixEvent.Content["body"]; matrixEvent.Type == "m.room.message" && ok {
		event.Text = body.(string)
	}

	return event
}

func (e messageEvent) Layout(gtx g, theme *material.Theme) d {
	align := layout.W
	bgcol := theme.Palette.ContrastBg
	if e.Own {
		align = layout.E
		bgcol = util.NRGB(0xeeeeee)
	}

	return align.Layout(gtx, func(gtx g) d {
		bg := fwidget.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),
		}

		var msgWidth int
		timecol := util.NRGBA(0xaaaaaaaa)

		return bg.Layout(gtx, func(gtx g) d {
			return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
				layout.Rigid(func(gtx g) d {
					senderName := e.Sender.Displayname()
					lbl := material.Body2(theme, senderName)
					// TODO
					// lbl.Color = initialColor(senderName[0])
					return lbl.Layout(gtx)
				}),

				layout.Rigid(func(gtx g) d {
					return e.layoutRoomEventContent(gtx, theme, &msgWidth)
				}),

				layout.Rigid(func(gtx g) 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(gtx g) d {
						time := util.FormatTime(e.Time)
						lbl := material.Caption(theme, time)
						lbl.Color = timecol
						return lbl.Layout(gtx)
					})
					children = append(children, child)

					return f.Layout(gtx, children...)
				}),
			)
		})
	})
}

func (e messageEvent) layoutRoomEventContent(gtx g, theme *material.Theme, msgWidth *int) d {
	lbl := material.Body2(theme, e.Text)
	if e.Own {
		lbl.Color = util.NRGB(0x000000)
	} else {
		lbl.Color = util.NRGB(0xffffff)
	}

	if e.Invalid {
		lbl.Color = util.NRGB(0xff0000)
	}

	dims := lbl.Layout(gtx)
	dims.Size.Y += gtx.Px(unit.Dp(4))
	*msgWidth = dims.Size.X
	return dims
}

M view/roomhistory/roomhistory.go => view/roomhistory/roomhistory.go +85 -377
@@ 2,12 2,10 @@ package roomhistory

import (
	"errors"
	"fmt"
	"time"

	fwidget "git.sr.ht/~f4814n/frost/widget"

	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/util"

	"golang.org/x/exp/shiny/materialdesign/icons"


@@ 16,7 14,9 @@ import (
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"go.uber.org/zap"
)



@@ 31,6 31,11 @@ type sentRoomEvent struct {

func (e sentRoomEvent) FrostEvent() {}

const (
	tagViewRoomMembers int = iota
	tagRoomSettings
)

type view struct {
	rx, tx chan frost.Event



@@ 44,29 49,55 @@ type view struct {
	history     matrix.History
	roomHistory *roomHistory

	backButton     *widget.Clickable
	settingsButton *widget.Clickable
	userListButton *widget.Clickable
	appBar                         *materials.AppBar
	userListButton, settingsButton *widget.Clickable

	messageComposer *messageComposer

	logger *zap.Logger
}

func New(self matrix.User, room matrix.Room, theme *material.Theme, logger *zap.Logger) frost.View {
	return &view{
func New(self matrix.User, room matrix.Room, modalLayer *materials.ModalLayer, theme *material.Theme, logger *zap.Logger) frost.View {
	appBar := materials.NewAppBar(modalLayer)

	view := &view{
		matrixEvents:    make(chan matrix.Event, 100),
		pastEvents:      make(chan matrix.Event, 100),
		room:            room,
		roomHistory:     newRoomHistory(self.ID, theme, logger),
		history:         room.History(),
		backButton:      new(widget.Clickable),
		settingsButton:  new(widget.Clickable),
		userListButton:  new(widget.Clickable),
		messageComposer: newMessageComposer(theme),
		logger:          logger,
		userListButton:  new(widget.Clickable),
		settingsButton:  new(widget.Clickable),
		theme:           theme,
		appBar:          appBar,
		messageComposer: newMessageComposer(),
	}

	appBar.NavigationIcon = util.MustIcon(icons.NavigationArrowBack)
	appBar.Title = room.Displayname()
	appBar.SetActions([]materials.AppBarAction{
		materials.SimpleIconAction(
			theme,
			view.userListButton,
			util.MustIcon(icons.ActionViewList),
			materials.OverflowAction{
				Name: "View room members",
				Tag:  tagViewRoomMembers,
			},
		),
		materials.SimpleIconAction(
			theme,
			view.settingsButton,
			util.MustIcon(icons.ActionSettings),
			materials.OverflowAction{
				Name: "Room settings",
				Tag:  tagRoomSettings,
			},
		),
	}, nil)

	return view
}

func (r *view) Run(rx, tx chan frost.Event) {


@@ 94,22 125,35 @@ func (r *view) Run(rx, tx chan frost.Event) {
	}
}

func (r *view) update() {
	if r.backButton.Clicked() {
		r.tx <- frost.CloseRoomhistoryEvent{}
	}

func (r *view) update(gtx g) {
	if r.settingsButton.Clicked() {
		r.logger.Warn("Not implemented")
	}

	if r.userListButton.Clicked() {
		r.logger.Info("Opening UsersView")
		r.logger.Info("Opening memberlist")
		r.tx <- frost.ShowMemberlistEvent{
			Room: r.room,
		}
	}

	for _, event := range r.appBar.Events(gtx) {
		switch event := event.(type) {
		case materials.AppBarNavigationClicked:
			r.tx <- frost.CloseRoomhistoryEvent{}
		case materials.AppBarOverflowActionClicked:
			switch event.Tag.(int) {
			case tagViewRoomMembers:
				r.logger.Info("Opening memberlist")
				r.tx <- frost.ShowMemberlistEvent{
					Room: r.room,
				}
			case tagRoomSettings:
				r.logger.Warn("Not implemented")
			}
		}
	}

	if r.messageComposer.Submitted() {
		r.messageComposer.Disable()



@@ 150,44 194,20 @@ func (r *view) Stop() {
}

func (r *view) Layout(gtx g) d {
	r.update()
	r.update(gtx)

	return layout.Flex{Alignment: layout.Start, Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(r.topbar),
		layout.Rigid(fwidget.LayoutTheme(r.appBar.Layout, r.theme)),
		layout.Flexed(1, r.roomHistory.Layout),
		layout.Rigid(r.messageComposer.Layout),
	)
}
		layout.Rigid(func(gtx g) d {
			in := layout.Inset{
				Top:    unit.Dp(8),
				Left:   unit.Dp(8),
				Right:  unit.Dp(8),
				Bottom: unit.Dp(4),
			}

func (r *view) topbar(gtx g) d {
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(fwidget.Fill{Color: r.theme.Palette.ContrastBg}.Layout),
		layout.Stacked(func(gtx g) d {
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(func(gtx g) d {
					icon := util.MustIcon(icons.NavigationArrowBack)
					btn := material.IconButton(r.theme, r.backButton, icon)
					return btn.Layout(gtx)
				}),
				layout.Flexed(1, func(gtx g) d {
					gtx.Constraints.Min.X = gtx.Constraints.Max.X
					lbl := material.H6(r.theme, r.room.Displayname())
					lbl.Color = util.NRGB(0xffffff)
					lbl.MaxLines = 1
					return lbl.Layout(gtx)
					// return d{Size: image.Point{X: gtx.Constraints.Max.X}}
				}),
				layout.Rigid(func(gtx g) d {
					icon := util.MustIcon(icons.ActionViewList)
					btn := material.IconButton(r.theme, r.userListButton, icon)
					return btn.Layout(gtx)
				}),
				layout.Rigid(func(gtx g) d {
					icon := util.MustIcon(icons.ActionSettings)
					btn := material.IconButton(r.theme, r.settingsButton, icon)
					return btn.Layout(gtx)
				}),
			)
			return in.Layout(gtx, fwidget.LayoutTheme(r.messageComposer.Layout, r.theme))
		}),
	)
}


@@ 197,7 217,7 @@ func (r *view) topbar(gtx g) d {
type roomHistory struct {
	user   string
	list   *layout.List
	events []matrix.Event
	events []fwidget.ThemedWidget
	logger *zap.Logger
	theme  *material.Theme
}


@@ 205,7 225,6 @@ type roomHistory struct {
func newRoomHistory(user string, theme *material.Theme, logger *zap.Logger) *roomHistory {
	return &roomHistory{
		user:   user,
		events: make([]matrix.Event, 0),
		list:   &layout.List{Axis: layout.Vertical, ScrollToEnd: true},
		logger: logger,
		theme:  theme,


@@ 213,11 232,21 @@ func newRoomHistory(user string, theme *material.Theme, logger *zap.Logger) *roo
}

func (w *roomHistory) AddEvent(event matrix.Event) {
	w.events = append(w.events, event)
	switch event := event.(type) {
	case matrix.RoomEvent:
		w.events = append(w.events, newMessageEvent(event.Sender.ID == w.user, event).Layout)
	case matrix.StateEvent:
		w.events = append(w.events, newStatusEvent(event).Layout)
	}
}

func (w *roomHistory) AddPastEvent(event matrix.Event) {
	w.events = append([]matrix.Event{event}, w.events...)
	switch event := event.(type) {
	case matrix.RoomEvent:
		w.events = append([]fwidget.ThemedWidget{newMessageEvent(event.Sender.ID == w.user, event).Layout}, w.events...)
	case matrix.StateEvent:
		w.events = append([]fwidget.ThemedWidget{newStatusEvent(event).Layout}, w.events...)
	}
}

// Layout implements the layout.Widget interface


@@ 229,329 258,8 @@ func (w *roomHistory) Layout(gtx g) d {
}

func (w *roomHistory) element(gtx g, index int) d {
	msg := w.events[index]
	in := layout.Inset{Top: unit.Dp(16), Left: unit.Dp(16), Right: unit.Dp(40)}
	if ownEvent(w.user, msg) {
		in.Left, in.Right = in.Right, in.Left
	}
	return in.Layout(gtx, event{
		Event:  msg,
		Own:    ownEvent(w.user, msg),
		logger: w.logger.Named("event"),
		Theme:  w.theme,
	}.Layout)
}

// event is a widget that displays a single event in the room history
type event struct {
	Event matrix.Event
	Own   bool
	Theme *material.Theme

	logger *zap.Logger
}

func (e event) Layout(gtx g) d {
	switch e.Event.(type) {
	case matrix.StateEvent:
		return e.layoutStateEvent(gtx)
	case matrix.RoomEvent:
		return e.layoutRoomEvent(gtx)
	default:
		panic(fmt.Sprintf("Should not have event in room history: %#v", e.Event))
	}
}

func (e event) layoutStateEvent(gtx g) d {
	align := layout.Center
	bgcol := util.NRGB(0xeeeeee)

	return align.Layout(gtx, func(gtx g) d {
		bg := fwidget.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),
		}

		return bg.Layout(gtx, e.layoutStateEventContent)
	})
}

func (e event) layoutStateEventContent(gtx g) d {
	ev := e.Event.(matrix.StateEvent)
	senderName := ev.Sender.Displayname()

	switch e.Event.Base().Type {
	case "m.room.member":
		lbl := material.Body1(e.Theme, fmt.Sprintf("%s changed their profile", ev.StateKey))
		return lbl.Layout(gtx)
	case "m.room.create":
		lbl := material.Body1(e.Theme, fmt.Sprintf("%s created the room", senderName))
		return lbl.Layout(gtx)
	case "m.room.power_levels":
		lbl := material.Body1(e.Theme, fmt.Sprintf("Room power levels were changed by %s", senderName))
		return lbl.Layout(gtx)
	case "m.room.join_rules":
		lbl := material.Body1(e.Theme, fmt.Sprintf("Join rules were changed by %s", senderName))
		return lbl.Layout(gtx)
	case "m.room.name":
		lbl := material.Body1(e.Theme, fmt.Sprintf("%s changed the room name to \"%s\"",
			senderName, ev.Room.Displayname()))
		return lbl.Layout(gtx)
	case "m.room.topic":
		var str string
		if topic, ok := ev.Content["topic"]; ok {
			str = fmt.Sprintf("%s changed the room topic to \"%s\"", senderName, topic)
		} else {
			str = fmt.Sprintf("%s removed the room topic", senderName)
			e.logger.Debug("Invalid event")
		}
		lbl := material.Body1(e.Theme, str)
		return lbl.Layout(gtx)
	case "m.room.guest_access":
		var str string
		if access, ok := ev.Content["guest_access"]; ok && access == "can_join" || access == "forbidden" {
			if access == "can_join" {
				str = fmt.Sprintf("%s allowed guest to join", senderName)
			} else {
				str = fmt.Sprintf("%s disallowed guests to join", senderName)
			}
		} else {
			e.logger.Debug("Invalid event")
			str = "Invalid"
		}
		return material.Body1(e.Theme, str).Layout(gtx)

	case "m.room.history_visibility":
		var str string
		if access, ok := ev.Content["history_visibility"]; ok {
			str = fmt.Sprintf("%s set the history visibility to \"%s\"", senderName, access)
		} else {
			e.logger.Debug("Invalid event")
			str = "Invalid"
		}
		return material.Body1(e.Theme, str).Layout(gtx)
	case "m.room.canonical_alias":
		var str string
		if alias, ok := ev.Content["canonical_alias"]; ok {
			str = fmt.Sprintf("%s changed the canonical alias to \"%s\"", senderName, alias)
		} else {
			str = fmt.Sprintf("%s set no or removed the canonical alias", senderName)
		}
		return material.Body1(e.Theme, str).Layout(gtx)
	default:
		lbl := material.Body1(e.Theme, "[Unformattable Event]")
		lbl.Color = util.NRGB(0xff0000)
		return lbl.Layout(gtx)
	}
}

func (e event) layoutRoomEvent(gtx g) d {
	align := layout.W
	bgcol := e.Theme.Palette.ContrastBg
	if e.Own {
		align = layout.E
		bgcol = util.NRGB(0xeeeeee)
	}

	return align.Layout(gtx, func(gtx g) d {
		bg := fwidget.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),
		}

		var msgWidth int
		timecol := util.NRGBA(0xaaaaaaaa)

		return bg.Layout(gtx, func(gtx g) d {
			return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
				layout.Rigid(func(gtx g) d {
					senderName := e.Event.(matrix.RoomEvent).Sender.Displayname()
					lbl := material.Body2(e.Theme, senderName)
					// TODO
					// lbl.Color = initialColor(senderName[0])
					return lbl.Layout(gtx)
				}),

				layout.Rigid(func(gtx g) d {
					return e.layoutRoomEventContent(gtx, &msgWidth)
				}),

				layout.Rigid(func(gtx g) 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(gtx g) d {
						time := util.FormatTime(util.EventTime(e.Event))
						lbl := material.Caption(e.Theme, time)
						lbl.Color = timecol
						return lbl.Layout(gtx)
					})
					children = append(children, child)

					return f.Layout(gtx, children...)
				}),
			)
		})
	})
}

func (e event) layoutRoomEventContent(gtx g, msgWidth *int) d {
	var (
		err     error
		content layout.Widget
	)

	e.Event, err = e.Event.(matrix.RoomEvent).Decrypted()

	if err != nil {
		lbl := material.Body2(e.Theme, err.Error())
		lbl.Color = util.NRGB(0xff0000)
		content = lbl.Layout
	} else {
		switch e.Event.Base().Type {
		case "m.room.message":
			lbl := material.Body2(e.Theme, e.Event.(matrix.RoomEvent).Content["body"].(string))
			if e.Own {
				lbl.Color = util.NRGB(0x000000)
			} else {
				lbl.Color = util.NRGB(0xffffff)
			}
			content = lbl.Layout
		default:
			lbl := material.Body1(e.Theme, "[Unformattable Event]")
			lbl.Color = util.NRGB(0xff0000)
			content = lbl.Layout
		}
	}

	dims := content(gtx)
	dims.Size.Y += gtx.Px(unit.Dp(4))
	*msgWidth = dims.Size.X
	return dims
}

type messageComposer struct {
	editor *widget.Editor
	submit *widget.Clickable
	theme  *material.Theme

	disabled  bool
	sendError error
}

func newMessageComposer(theme *material.Theme) *messageComposer {
	return &messageComposer{
		editor:   &widget.Editor{Submit: true, SingleLine: false},
		submit:   new(widget.Clickable),
		disabled: false,
		theme:    theme,
	}
}

func (m *messageComposer) Layout(gtx g) d {
	in := layout.Inset{
		Top:    unit.Dp(8),
		Left:   unit.Dp(8),
		Right:  unit.Dp(8),
		Bottom: unit.Dp(4),
	}

	if mh := gtx.Px(unit.Dp(200)); gtx.Constraints.Max.Y > mh {
		gtx.Constraints.Max.Y = mh
	}

	if m.disabled {
		gtx = gtx.Disabled()
	}

	var minHeight int
	return in.Layout(gtx, func(gtx g) d {
		return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
			layout.Flexed(1, func(gtx g) d {
				return m.layoutEditor(gtx, &minHeight)
			}),
			layout.Rigid(func(gtx g) d {
				dims := layout.Inset{Left: unit.Dp(8)}.Layout(gtx, func(gtx g) d {
					icon := util.MustIcon(icons.ContentSend)
					btn := material.IconButton(m.theme, m.submit, icon)
					btn.Size = unit.Dp(24)
					btn.Inset = layout.UniformInset(unit.Dp(6))
					return btn.Layout(gtx)
				})
				minHeight = dims.Size.Y
				return dims
			}),
		)
	})
}

func (m *messageComposer) layoutEditor(gtx g, minHeight *int) d {
	gtx.Constraints.Min.X = gtx.Constraints.Max.X

	bg := fwidget.Background{
		Color:  util.NRGB(0xeeeeee),
		Inset:  layout.Inset{Left: unit.Dp(8), Right: unit.Dp(8)},
		Radius: unit.Dp(10),
	}

	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx g) d {
			if m.sendError != nil {
				lbl := material.Body2(m.theme, m.sendError.Error())
				lbl.Color = util.NRGB(0xff0000)
				return lbl.Layout(gtx)
			}
			return d{}
		}),
		layout.Rigid(func(gtx g) d {
			return bg.Layout(gtx, func(gtx g) d {
				return layout.W.Layout(gtx, func(gtx g) d {
					gtx.Constraints.Min.X = gtx.Constraints.Max.X
					gtx.Constraints.Min.Y = *minHeight
					ed := material.Editor(m.theme, m.editor, "Send a message")
					ed.TextSize = unit.Sp(12)
					return ed.Layout(gtx)
				})
			})
		}),
	)
}

func (m *messageComposer) Submitted() bool {
	return m.submit.Clicked()
}

func (m *messageComposer) Text() string {
	return m.editor.Text()
}

func (m *messageComposer) Disable() {
	m.disabled = true
}

// Reenable activates the composer after a event has been sent. If the error
// is not nil, then the editor is cleared
func (m *messageComposer) Reenable(err error) {
	m.disabled = false
	m.sendError = err
	m.editor.SetText("")
	return in.Layout(gtx, fwidget.LayoutTheme(w.events[index], w.theme))
}

func ownEvent(me string, ev matrix.Event) bool {

M view/roomlist/roomlist.go => view/roomlist/roomlist.go +12 -29
@@ 2,22 2,23 @@ package roomlist

import (
	"fmt"
	"image"
	// "image"
	"unicode"

	"go.uber.org/zap"
	"golang.org/x/exp/shiny/materialdesign/icons"
	// "golang.org/x/exp/shiny/materialdesign/icons"

	"gioui.org/gesture"
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
	// "gioui.org/widget"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/util"
	fwidget "git.sr.ht/~f4814n/frost/widget"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
)

type (


@@ 33,21 34,24 @@ type view struct {
	cli      *matrix.Client
	roomList *RoomList

	menuButton *widget.Clickable

	theme *material.Theme

	appBar *materials.AppBar

	logger *zap.Logger
}

func New(cli *matrix.Client, theme *material.Theme, logger *zap.Logger) frost.View {
func New(cli *matrix.Client, modalLayer *materials.ModalLayer, theme *material.Theme, logger *zap.Logger) frost.View {
	appBar := materials.NewAppBar(modalLayer)
	appBar.Title = "Frost"

	return &view{
		cli:          cli,
		matrixEvents: make(chan matrix.Event, 100),
		roomList:     NewRoomList(logger, theme),
		menuButton:   new(widget.Clickable),
		theme:        theme,
		logger:       logger,
		appBar:       appBar,
	}
}



@@ 62,28 66,11 @@ func (l *view) Layout(gtx layout.Context) layout.Dimensions {
	l.update(gtx)

	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(l.topbar),
		layout.Rigid(fwidget.LayoutTheme(l.appBar.Layout, l.theme)),
		layout.Rigid(l.roomList.Layout),
	)
}

func (l *view) topbar(gtx g) d {
	return layout.Stack{Alignment: layout.SW}.Layout(gtx,
		layout.Expanded(fwidget.Fill{Color: l.theme.Palette.ContrastBg}.Layout),
		layout.Stacked(func(gtx g) d {
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(func(gtx g) d {
					btn := material.IconButton(l.theme, l.menuButton, util.MustIcon(icons.NavigationMenu))
					return btn.Layout(gtx)
				}),
				layout.Rigid(func(gtx g) d {
					return d{Size: image.Point{X: gtx.Constraints.Max.X}}
				}),
			)
		}),
	)
}

func (l *view) update(gtx g) {
	for _, room := range l.roomList.rooms {
		for _, e := range room.click.Events(gtx) {


@@ 91,10 78,6 @@ func (l *view) update(gtx g) {
				l.tx <- frost.ShowRoomEvent{
					Room: room.room,
				}
				// l.tx <- frost.LoadViewEvent{
				// 	Position: frost.Middle,
				// 	View:     NewRoomView(l.cli, room.room),
				// }
			}
		}
	}

A widget/theme.go => widget/theme.go +14 -0
@@ 0,0 1,14 @@
package widget

import (
	"gioui.org/layout"
	"gioui.org/widget/material"
)

type ThemedWidget func(gtx layout.Context, theme *material.Theme) layout.Dimensions

func LayoutTheme(widget ThemedWidget, theme *material.Theme) layout.Widget {
	return func(gtx layout.Context) layout.Dimensions {
		return widget(gtx, theme)
	}
}