~f4814n/frost

1870b0c3e7772205c410c029ce479bbaeda94ca2 — Fabian Geiselhart 3 months ago f03d7b5
Top level layout rework
12 files changed, 363 insertions(+), 752 deletions(-)

A cmd/frost/app.go
D cmd/frost/debug.go
D cmd/frost/events.go
D cmd/frost/main.go
D cmd/frost/merge_view.go
A component/sync/sync.go
M events.go
M view/memberlist/memberlist.go
D view/responsive/view.go
M view/roomhistory/event.go
M view/roomhistory/roomhistory.go
M view/roomlist/roomlist.go
A cmd/frost/app.go => cmd/frost/app.go +317 -0
@@ 0,0 1,317 @@
package main

import (
	"fmt"
	"net/http"

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

	"gioui.org/font/gofont"

	"gioui.org/app"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"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/component/sync"
	"git.sr.ht/~f4814n/frost/platform"
	"git.sr.ht/~f4814n/frost/util"
	"git.sr.ht/~f4814n/frost/view/login"
	"git.sr.ht/~f4814n/frost/view/memberlist"
	"git.sr.ht/~f4814n/frost/view/roomhistory"
	"git.sr.ht/~f4814n/frost/view/roomlist"
	fwidget "git.sr.ht/~f4814n/frost/widget"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"go.uber.org/zap"
)

type (
	g = layout.Context
	d = layout.Dimensions
)

type App struct {
	window *app.Window

	cli *matrix.Client

	rx, tx chan frost.Event

	config   platform.AppConfig
	platform platform.Platform
	logger   *zap.Logger

	cache frost.Component
	sync  frost.Component
	mux   *frost.Mux

	currentRoom matrix.Room

	bar            *materials.AppBar
	userListButton *widget.Clickable

	// On a widescreen desktop the layout is split into three columns.
	views [3]registeredView

	// singleView is True if the focused view should be the only visible view
	// regardless of screen size
	singleView bool

	modalLayer *materials.ModalLayer
	drawer     *materials.ModalNavDrawer
	theme      *material.Theme
}

type registeredView struct {
	id uint
	frost.View
}

func newApp() *App {
	platform, err := platform.Init()
	if err != nil {
		panic(err.Error())
	}

	logger := platform.Logger()

	a := &App{
		mux: frost.NewMux(),
		cli: matrix.NewClient(matrix.ClientOpts{
			HTTPClient: http.DefaultClient,
			Backend:    platform.Backend(),
			Logger:     MatrixLogger{logger.Named("matrix").Sugar()},
		}),
		window: app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		),
		platform:       platform,
		logger:         logger.Named("frost"),
		modalLayer:     materials.NewModal(),
		theme:          material.NewTheme(gofont.Collection()),
		userListButton: new(widget.Clickable),
	}

	a.bar = materials.NewAppBar(a.modalLayer)
	a.bar.Title = "Frost"
	a.bar.NavigationIcon = util.MustIcon(icons.NavigationMenu)

	a.drawer = materials.NewModalNav(a.modalLayer, "Frost", "Here be dragons")

	a.cache = cache.New(a.cli, platform.Cache(), a.logger.Named("component.cache"))
	a.sync = sync.New(a.cli, a.logger.Named("component.sync"))

	return a
}

func (a *App) run() error {
	var err error

	go a.mux.Run()

	_, a.rx, a.tx = a.mux.Register()

	_, rx, tx := a.mux.Register()
	go a.cache.Run(rx, tx)

	if a.config, err = a.platform.LoadConfig(); err != nil {
		a.logger.Warn("Failed to load config", zap.Error(err))
	}

	if a.config.Session != nil {
		err := a.cli.LoadToken(a.config.Session.MxID, a.config.Session.DeviceID)
		if err != nil {
			a.logger.Error("Failed to reuse session", zap.Error(err))
		}
		_, rx, tx := a.mux.Register()
		go a.sync.Run(rx, tx)

		id, rx, tx := a.mux.Register()
		a.views[0] = registeredView{
			id:   id,
			View: roomlist.New(a.cli, a.modalLayer, a.theme, a.logger.Named("roomlist")),
		}
		a.views[0].Run(rx, tx)
	} else {
		id, rx, tx := a.mux.Register()
		a.views[0] = registeredView{id: id, View: login.New(a.theme, a.logger.Named("login"))}
		go a.views[0].Run(rx, tx)
	}

	for {
		if err := a.handleEvents(); err != nil {
			return err
		}
	}
}

func (a *App) handleEvents() error {
	select {
	case e := <-a.window.Events():
		switch e := e.(type) {
		case system.FrameEvent:
			var ops op.Ops
			gtx := layout.NewContext(&ops, e)
			a.update(gtx)
			layout.Inset{
				Top:    e.Insets.Top,
				Bottom: e.Insets.Bottom,
				Left:   e.Insets.Left,
				Right:  e.Insets.Right,
			}.Layout(gtx, func(gtx g) d {
				a.Layout(gtx)
				return a.modalLayer.Layout(gtx, a.theme)
			})
			e.Frame(gtx.Ops)
		}
	case e := <-a.rx:
		fmt.Printf("%#v\n", e)
		switch e := e.(type) {
		case roomhistory.Show:
			a.currentRoom = e.Room

			a.stopView(2)
			a.stopView(1)

			self, _ := a.cli.User()

			id, rx, tx := a.mux.Register()
			a.views[1] = registeredView{
				id: id,
				View: roomhistory.New(
					self,
					e.Room,
					a.theme,
					a.logger.Named("roomhistory"),
				),
			}

			a.bar.Title = e.Room.Displayname()
			a.bar.SetActions([]materials.AppBarAction{
				materials.SimpleIconAction(
					a.theme,
					a.userListButton,
					util.MustIcon(icons.ActionViewList),
					materials.OverflowAction{
						Name: "View room members",
						Tag:  a.userListButton,
					},
				),
			}, nil)

			go a.views[1].Run(rx, tx)
		}
	}

	return nil
}

func (a *App) stopView(i int) {
	if a.views[i].View != nil {
		a.views[i].Stop()
		a.mux.Unregister(a.views[i].id)
		a.views[i].View = nil
	}
}

func (a *App) update(gtx g) {
	for _, event := range a.bar.Events(gtx) {
		switch event.(type) {
		case materials.AppBarNavigationClicked:
			a.drawer.Appear(gtx.Now)
		}
	}

	if a.userListButton.Clicked() && a.views[2].View == nil {
		print("memberlist")
		id, rx, tx := a.mux.Register()
		a.views[2] = registeredView{
			id:   id,
			View: memberlist.New(a.currentRoom, a.theme, a.logger.Named("memberlist")),
		}
		go a.views[2].Run(rx, tx)
	}
}

func (a *App) Layout(gtx layout.Context) layout.Dimensions {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(fwidget.LayoutTheme(a.bar.Layout, a.theme)),
		layout.Flexed(1, func(gtx g) d {
			switch {
			case gtx.Constraints.Max.X < gtx.Px(unit.Dp(720)):
				return a.layoutSmall(gtx)
			case gtx.Constraints.Max.X < gtx.Px(unit.Dp(1100)):
				return a.layoutMedium(gtx)
			default:
				return a.layoutLarge(gtx)
			}
		}),
	)
}

func (a *App) focused() registeredView {
	for i := len(a.views) - 1; i >= 0; i-- {
		if a.views[i].View != nil {
			return a.views[i]
		}
	}
	panic("No views")
}

func (a *App) layoutSmall(gtx g) d {
	return a.focused().Layout(gtx)
}

func (a *App) layoutMedium(gtx g) d {
	children := []layout.FlexChild{
		layout.Rigid(func(gtx g) d {
			gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
			return a.views[0].Layout(gtx)
		}),
	}

	if len(a.views) >= 2 {
		children = append(children, layout.Flexed(1, a.focused().Layout))
	}

	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...)
}

func (a *App) layoutLarge(gtx g) d {
	children := []layout.FlexChild{
		layout.Rigid(func(gtx g) d {
			gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
			return a.views[0].Layout(gtx)
		}),
	}

	if a.views[1].View != nil {
		children = append(children, layout.Flexed(1, a.views[1].Layout))

		if a.views[2].View != nil {
			children = append(children, layout.Rigid(func(gtx g) d {
				gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
				gtx.Constraints.Min.X = gtx.Px(unit.Dp(400))
				return a.focused().Layout(gtx)
			}))
		}
	}

	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...)
}

func main() {
	go func() {
		a := newApp()
		if err := a.run(); err != nil {
			a.logger.Fatal("Error in main loop", zap.Error(err))
		}
	}()
	app.Main()
}

D cmd/frost/debug.go => cmd/frost/debug.go +0 -12
@@ 1,12 0,0 @@
// +build debug

package main

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

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

D cmd/frost/events.go => cmd/frost/events.go +0 -1
@@ 1,1 0,0 @@
package main

D cmd/frost/main.go => cmd/frost/main.go +0 -235
@@ 1,235 0,0 @@
package main

import (
	"context"
	"net/http"

	"gioui.org/app"
	"gioui.org/font/gofont"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/component/cache"
	"git.sr.ht/~f4814n/frost/platform"
	"git.sr.ht/~f4814n/frost/view/login"
	"git.sr.ht/~f4814n/frost/view/responsive"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"go.uber.org/zap"
)

var theme *material.Theme

type (
	Gtx  = layout.Context
	Dims = layout.Dimensions
)

const (
	tagNavSettings byte = iota
	tagNavRooms
)

type App struct {
	window   *app.Window
	cli      *matrix.Client
	rx, tx   chan frost.Event
	config   platform.AppConfig
	platform platform.Platform
	logger   *zap.Logger
	cache    frost.Component
	mux      *frost.Mux

	modalLayer *materials.ModalLayer
	view       frost.View
	viewID     uint
}

func newApp() *App {
	platform, err := platform.Init()
	if err != nil {
		panic(err.Error())
	}

	backend := platform.Backend()
	logger := platform.Logger()
	platformCache := platform.Cache()

	a := &App{
		mux: frost.NewMux(),
		cli: matrix.NewClient(matrix.ClientOpts{
			HTTPClient: http.DefaultClient,
			Backend:    backend,
			Logger:     MatrixLogger{logger.Named("matrix").Sugar()},
		}),
		window: app.NewWindow(
			app.Size(unit.Dp(400), unit.Dp(800)),
			app.Title("Frost"),
		),
		platform:   platform,
		logger:     logger.Named("frost"),
		modalLayer: materials.NewModal(),
	}

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

	return a
}

func (a *App) run() error {
	theme = material.NewTheme(gofont.Collection())

	go a.mux.Run()

	_, rx, tx := a.mux.Register()
	a.rx = rx
	a.tx = tx

	// Cache
	_, rx, tx = a.mux.Register()
	go a.cache.Run(rx, tx)

	matrixEvents := make(chan matrix.Event, 100)
	a.cli.Notify(matrixEvents)

	for {
		select {
		case e := <-a.window.Events():
			switch e := e.(type) {
			case system.StageEvent:
				var err error
				a.config, err = a.platform.LoadConfig()
				if err != nil {
					a.logger.Warn("Failed to load config", zap.Error(err))
				}

				a.initialView()
			case system.FrameEvent:
				var ops op.Ops
				gtx := layout.NewContext(&ops, e)
				layout.Inset{
					Top:    e.Insets.Top,
					Bottom: e.Insets.Bottom,
					Left:   e.Insets.Left,
					Right:  e.Insets.Right,
				}.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 {
				case system.CommandBack:
					e.Cancel = true
					a.tx <- frost.BackEvent{}
					a.rx <- frost.InvalidationEvent{}
				}
			case system.DestroyEvent:
				return e.Err
			}

		case e := <-a.rx:
			switch e := e.(type) {
			case login.StartEvent:
				go a.login(e)
			case frost.InvalidationEvent:
				a.window.Invalidate()
			}

		case <-matrixEvents:
			a.window.Invalidate()
		}
	}
}

func (a *App) Layout(gtx Gtx) Dims {
	return a.view.Layout(gtx)
}

func (a *App) login(event login.StartEvent) {
	err := a.cli.Login(event.Username, event.Password)
	if err != nil {
		a.logger.Debug("Failed to log in", zap.Error(err))
		a.tx <- login.ErrorEvent{Err: err}
		return
	}
	a.logger.Debug("Successfully authenticated")

	a.config.Session = &platform.SessionConfig{
		MxID:     a.cli.MxID(),
		DeviceID: a.cli.DeviceID(),
	}
	err = a.platform.FlushConfig(a.config)
	if err != nil {
		a.logger.Debug("Failed to flush config", zap.Error(err))
	}

	a.view.Stop()
	a.mux.Unregister(a.viewID)
	a.view = nil

	a.view = responsive.New(a.cli, a.mux, theme, a.logger.Named("roomlist"))
	id, rx, tx := a.mux.Register()
	a.viewID = id
	go a.view.Run(rx, tx)

	matrixEvents := make(chan matrix.Event, 1000)
	a.cli.Notify(matrixEvents)

	a.sync()
}

func (a *App) initialView() {
	if a.config.Session != nil {
		err := a.cli.LoadToken(a.config.Session.MxID, a.config.Session.DeviceID)
		if err != nil {
			a.logger.Error("Failed to reuse session", zap.Error(err))
		}
		a.sync()

		a.view = responsive.New(a.cli, a.mux, theme, a.logger.Named("responsive"))
		id, rx, tx := a.mux.Register()
		a.viewID = id
		go a.view.Run(rx, tx)
	} else {
		a.view = login.New(theme, a.logger.Named("login"))
		id, rx, tx := a.mux.Register()
		a.viewID = id
		go a.view.Run(rx, tx)
	}

	a.rx <- frost.InvalidationEvent{}
}

func (a *App) sync() {
	// TODO Gracefully stop this goroutine
	go a.cli.Sync(context.TODO(), &matrix.SyncOpts{
		OnError: func(err error) error {
			a.logger.Warn("Sync error", zap.Error(err))
			return nil
		},
		Timeout: 10000,
	})

	// TODO Gracefully stop this goroutine
	go func() {
		c := make(chan matrix.Event, 100)
		a.cli.Notify(c)
		for range c {
			a.rx <- frost.InvalidationEvent{}
		}
	}()
}

func main() {
	go func() {
		a := newApp()
		if err := a.run(); err != nil {
			a.logger.Fatal("Error in main loop", zap.Error(err))
		}
	}()
	app.Main()
}

D cmd/frost/merge_view.go => cmd/frost/merge_view.go +0 -178
@@ 1,178 0,0 @@
package main
//
import (
	"image"

	"gioui.org/layout"
	"gioui.org/unit"
	fwidget "git.sr.ht/~f4814n/frost/widget"
)
//
// type InvalidationEvent struct{}
//
// type BackEvent struct{}
//
// type Position int
//
// const (
// 	Left Position = iota
// 	Middle
// 	Right
// 	Overlay
// )
//
// type LoadViewEvent struct {
// 	Position Position
// 	View     View
// }
//
// type UnloadViewEvent struct {
// 	Position Position
// }
//
// // MergeView implements a responsive three grid view.
// type MergeView struct {
// 	rx, tx chan Event
// 	stop chan struct{}
//
//
// 	cli *matrix.Client
//
// 	left, right, middle, overlay View
// }
//
// func NewMergeView(cli *matrix.Client) *MergeView {
// 	return &MergeView{
// 		cli: cli,
//
// 		stop: make(chan struct{}),
// 	}
// }
//
// func (o *MergeView) Start(rx, tx chan Event) {
// 	o.rx, o.tx = rx, tx
// }
//
// // focused returns the rightmost View
// func (o *MergeView) Layout(gtx Gtx) Dims {
// 	leftsize := gtx.Px(unit.Dp(400))
// 	rightsize := gtx.Px(unit.Dp(400))
//
// 	if o.overlay != nil {
// 		return o.overlay.Layout(gtx)
// 	}
//
// 	if o.left == nil && o.middle == nil && o.right == nil {
// 		return NoView{}.Layout(gtx)
// 	}
//
// 	if gtx.Constraints.Max.X < gtx.Px(unit.Dp(720)) {
// 		if view := o.focused(); view != nil {
// 			return view.Layout(gtx)
// 		}
// 		return NoView{}.Layout(gtx)
// 	} else if gtx.Constraints.Max.X < gtx.Px(unit.Dp(1100)) {
// 		return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// 			layout.Rigid(func(gtx Gtx) Dims {
// 				gtx.Constraints.Max.X = leftsize
// 				return o.left.Layout(gtx)
// 			}),
// 			layout.Flexed(1, func(gtx Gtx) Dims {
// 				if view := o.focused(); view != nil && view != o.left {
// 					return view.Layout(gtx)
// 				} else {
// 					return NoView{}.Layout(gtx)
// 				}
// 			}),
// 		)
// 	} else {
// 		return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// 			layout.Rigid(func(gtx Gtx) Dims {
// 				gtx.Constraints.Max.X = leftsize
// 				return o.left.Layout(gtx)
// 			}),
// 			layout.Flexed(1, func(gtx Gtx) Dims {
// 				if o.middle != nil {
// 					return o.middle.Layout(gtx)
// 				}
// 				return NoView{}.Layout(gtx)
// 			}),
// 			layout.Rigid(func(gtx Gtx) Dims {
// 				if o.right != nil {
// 					gtx.Constraints.Max.X = rightsize
// 					return o.right.Layout(gtx)
// 				}
// 				return Dims{}
// 			}),
// 		)
// 	}
// }
//
// // func (o *MergeView) receiveEvent(event interface{}) {
// // 	switch event := event.(type) {
// // 	case LoadViewEvent:
// // 		view := event.View
// //
// // 		switch event.Position {
// // 		case Left:
// // 			view.Start(o.txL, o.rxL)
// // 			o.left = view
// // 		case Right:
// // 			view.Start(o.txR, o.rxR)
// // 			o.right = view
// // 		case Middle:
// // 			view.Start(o.txM, o.rxM)
// // 			o.middle = view
// // 		case Overlay:
// // 			view.Start(o.txO, o.rxO)
// // 			o.overlay = view
// // 		}
// // 	case UnloadViewEvent:
// // 		switch event.Position {
// // 		case Left:
// // 			o.left.Stop()
// // 			o.left = nil
// // 		case Right:
// // 			o.right.Stop()
// // 			o.right = nil
// // 		case Middle:
// // 			o.middle.Stop()
// // 			o.middle = nil
// // 		case Overlay:
// // 			o.overlay.Stop()
// // 			o.overlay = nil
// // 		}
// // 	case BackEvent:
// // 		o.focused().Stop()
// // 		if o.focused() == o.right {
// // 			o.right = nil
// // 		} else {
// // 			o.middle = nil
// // 		}
// // 	default:
// // 		o.txL <- event
// // 		o.txM <- event
// // 		o.txR <- event
// // 		o.txO <- event
// // 		o.tx <- event
// // 	}
// // }
// //
// // func (o *MergeView) Stop() {
// // 	close(o.stop)
// // }
//
type NoView struct{}

func (v NoView) Layout(gtx Gtx) Dims {
	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			return layout.Stack{Alignment: layout.SW}.Layout(gtx,
				layout.Expanded(fwidget.Fill{theme.Palette.ContrastBg}.Layout),
				layout.Stacked(func(gtx Gtx) Dims {
					return Dims{Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Px(unit.Dp(48))}}
				}),
			)
		}),
	)
}

A component/sync/sync.go => component/sync/sync.go +34 -0
@@ 0,0 1,34 @@
package sync

import (
	"context"

	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/matrix"
	"go.uber.org/zap"
)

type component struct {
	cli    *matrix.Client
	logger *zap.Logger
}

func New(cli *matrix.Client, logger *zap.Logger) frost.Component {
	return component{
		cli:    cli,
		logger: logger,
	}
}

func (c component) Run(rx, tx chan frost.Event) {
	go c.cli.Sync(context.TODO(), &matrix.SyncOpts{
		OnError: func(err error) error {
			c.logger.Warn("Sync error", zap.Error(err))
			return nil
		},
		Timeout: 10000,
	})
}

func (c component) Stop() {
}

M events.go => events.go +0 -6
@@ 2,12 2,6 @@ package frost

import "git.sr.ht/~f4814n/matrix"

type ShowRoomEvent struct {
	Room matrix.Room
}

func (s ShowRoomEvent) FrostEvent() {}

type CloseMemberlistEvent struct{}

func (s CloseMemberlistEvent) FrostEvent() {}

M view/memberlist/memberlist.go => view/memberlist/memberlist.go +2 -23
@@ 13,12 13,9 @@ import (
	"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"
	"golang.org/x/image/draw"
	_ "golang.org/x/image/tiff"


@@ 54,15 51,9 @@ type view struct {
	theme *material.Theme

	logger *zap.Logger

	appBar *materials.AppBar
}

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"

func New(room matrix.Room, theme *material.Theme, logger *zap.Logger) frost.View {
	return &view{
		matrixEvents: make(chan matrix.Event, 100),
		avatars:      make(map[string]image.Image),


@@ 70,7 61,6 @@ func New(room matrix.Room, modalLayer *materials.ModalLayer, theme *material.The
		room:         room,
		logger:       logger,
		theme:        theme,
		appBar:       appBar,
	}
}



@@ 160,12 150,6 @@ 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 {


@@ 192,12 176,7 @@ loop:
func (v *view) Layout(gtx layout.Context) layout.Dimensions {
	v.update(gtx)

	return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
		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)
		}),
	)
	return v.userList.Layout(gtx, len(v.users), v.listElem)
}

func (v *view) listElem(gtx g, index int) d {

D view/responsive/view.go => view/responsive/view.go +0 -210
@@ 1,210 0,0 @@
package responsive

import (
	"sync"
	"time"

	"git.sr.ht/~f4814n/frost/view/memberlist"
	"git.sr.ht/~f4814n/frost/view/roomhistory"
	"git.sr.ht/~f4814n/frost/view/roomlist"

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

type (
	g = layout.Context
	d = layout.Dimensions
)

const (
	left   = 0
	middle = 1
	right  = 2
)

// Tags used for the NavDrawer
const (
	tagNavSettings byte = iota
	tagNavRooms
)

type view struct {
	views      []idView
	modalLayer *materials.ModalLayer
	drawer     *materials.ModalNavDrawer
	theme      *material.Theme
	logger     *zap.Logger
	cli        *matrix.Client
	mut        sync.Mutex
	mux        *frost.Mux
}

type idView struct {
	frost.View
	ID uint
}

func New(cli *matrix.Client, mux *frost.Mux, theme *material.Theme, logger *zap.Logger) frost.View {
	view := &view{
		logger:     logger,
		modalLayer: materials.NewModal(),
		theme:      theme,
		views:      make([]idView, 1),
		mux:        mux,
		cli:        cli,
	}

	// Setup Drawer
	view.drawer = materials.NewModalNav(view.modalLayer, "Frost", "Here be dragons")

	view.drawer.AddNavItem(materials.NavItem{
		Name: "Rooms",
		Icon: util.MustIcon(icons.SocialGroup),
		Tag:  tagNavRooms,
	})

	view.drawer.AddNavItem(materials.NavItem{
		Name: "Settings",
		Icon: util.MustIcon(icons.ActionSettings),
		Tag:  tagNavSettings,
	})

	// Add roomlist as the leftmost view
	id, rx, tx := view.mux.Register()
	view.views[left] = idView{roomlist.New(cli, view.modalLayer, theme, view.logger.Named("roomlist")), id}
	go view.views[left].Run(rx, tx)

	return view
}

func (v *view) view(i int) (idView, bool) {
	if len(v.views) >= i+1 {
		return v.views[i], true
	}
	return idView{}, false
}

func (v *view) closeView(location int) {
	if len(v.views) > location {
		v.views[location].Stop()
		v.mux.Unregister(v.views[location].ID)
		v.views = v.views[:len(v.views)-1]
	}
}

func (v *view) addView(view idView, location int) {
	if view, ok := v.view(location); ok {
		view.Stop()
		v.mux.Unregister(view.ID)
	}

	if len(v.views) <= location {
		v.views = append(v.views, view)
	} else {
		v.views[location] = view
	}
}

func (v *view) Run(rx chan frost.Event, tx chan frost.Event) {
	for event := range rx {
		switch e := event.(type) {
		case frost.ToggleNavigation:
			v.drawer.Appear(time.Now())
		case frost.ShowRoomEvent:
			self, _ := v.cli.User()
			id, rx, tx := v.mux.Register()
			view := idView{
				roomhistory.New(
					self, e.Room, v.modalLayer, v.theme, v.logger.Named("roomhistory"),
				),
				id,
			}
			v.addView(view, middle)
			go view.Run(rx, tx)
		case frost.ShowMemberlistEvent:
			id, rx, tx := v.mux.Register()
			view := idView{
				memberlist.New(e.Room, v.modalLayer, v.theme, v.logger.Named("memberlist")),
				id,
			}
			v.addView(view, right)
			go view.Run(rx, tx)
		case frost.CloseRoomhistoryEvent:
			v.closeView(middle)
		case frost.CloseMemberlistEvent:
			v.closeView(right)
		case frost.BackEvent:
			v.closeView(len(v.views) - 1)
		}
	}
}

func (v *view) focused() frost.View {
	return v.views[len(v.views)-1]
}

func (v *view) Layout(gtx layout.Context) layout.Dimensions {
	v.mut.Lock()
	defer v.mut.Unlock()

	switch {
	case gtx.Constraints.Max.X < gtx.Px(unit.Dp(720)):
		return v.layoutSmall(gtx)
	case gtx.Constraints.Max.X < gtx.Px(unit.Dp(1100)):
		return v.layoutMedium(gtx)
	default:
		return v.layoutLarge(gtx)
	}
}

func (v *view) layoutSmall(gtx g) d {
	return v.focused().Layout(gtx)
}

func (v *view) layoutMedium(gtx g) d {
	children := []layout.FlexChild{
		layout.Rigid(func(gtx g) d {
			gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
			return v.views[left].Layout(gtx)
		}),
	}

	if len(v.views) >= 2 {
		children = append(children, layout.Flexed(1, v.focused().Layout))
	}

	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...)
}

func (v *view) layoutLarge(gtx g) d {
	children := []layout.FlexChild{
		layout.Rigid(func(gtx g) d {
			gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
			return v.views[left].Layout(gtx)
		}),
	}

	if len(v.views) >= 2 {
		children = append(children, layout.Flexed(1, v.views[middle].Layout))
	}

	if len(v.views) >= 3 {
		children = append(children, layout.Rigid(func(gtx g) d {
			gtx.Constraints.Max.X = gtx.Px(unit.Dp(400))
			return v.focused().Layout(gtx)
		}))
	}

	return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, children...)
}

func (v *view) Stop() {
}

M view/roomhistory/event.go => view/roomhistory/event.go +6 -0
@@ 14,6 14,12 @@ import (
	"git.sr.ht/~f4814n/matrix"
)

type Show struct {
	Room matrix.Room
}

func (s Show) FrostEvent() {}

type statusEvent struct {
	Text    string
	Invalid bool

M view/roomhistory/roomhistory.go => view/roomhistory/roomhistory.go +1 -67
@@ 6,17 6,11 @@ import (

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

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

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

	"gioui.org/layout"
	"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"
)



@@ 49,17 43,12 @@ type view struct {
	history     matrix.History
	roomHistory *roomHistory

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

	messageComposer *messageComposer

	logger *zap.Logger
}

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

func New(self matrix.User, room matrix.Room, theme *material.Theme, logger *zap.Logger) frost.View {
	view := &view{
		matrixEvents:    make(chan matrix.Event, 100),
		pastEvents:      make(chan matrix.Event, 100),


@@ 67,36 56,10 @@ func New(self matrix.User, room matrix.Room, modalLayer *materials.ModalLayer, t
		roomHistory:     newRoomHistory(self.ID, theme, logger),
		history:         room.History(),
		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
}



@@ 126,34 89,6 @@ func (r *view) Run(rx, tx chan frost.Event) {
}

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

	if r.userListButton.Clicked() {
		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()



@@ 197,7 132,6 @@ func (r *view) Layout(gtx g) d {
	r.update(gtx)

	return layout.Flex{Alignment: layout.Start, Axis: layout.Vertical}.Layout(gtx,
		layout.Rigid(fwidget.LayoutTheme(r.appBar.Layout, r.theme)),
		layout.Flexed(1, r.roomHistory.Layout),
		layout.Rigid(func(gtx g) d {
			in := layout.Inset{

M view/roomlist/roomlist.go => view/roomlist/roomlist.go +3 -20
@@ 16,10 16,10 @@ import (
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/frost"
	"git.sr.ht/~f4814n/frost/util"
	"git.sr.ht/~f4814n/frost/view/roomhistory"
	fwidget "git.sr.ht/~f4814n/frost/widget"
	"git.sr.ht/~f4814n/matrix"
	"git.sr.ht/~whereswaldon/materials"
	"golang.org/x/exp/shiny/materialdesign/icons"
)

type (


@@ 37,23 37,16 @@ type view struct {

	theme *material.Theme

	appBar *materials.AppBar

	logger *zap.Logger
}

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

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



@@ 67,30 60,20 @@ func (l *view) Run(rx, tx chan frost.Event) {
func (l *view) Layout(gtx layout.Context) layout.Dimensions {
	l.update(gtx)

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

func (l *view) update(gtx g) {
	for _, room := range l.roomList.rooms {
		for _, e := range room.click.Events(gtx) {
			if e.Type == gesture.TypeClick {
				l.tx <- frost.ShowRoomEvent{
				l.tx <- roomhistory.Show{
					Room: room.room,
				}
			}
		}
	}

	for _, event := range l.appBar.Events(gtx) {
		switch event.(type) {
		case materials.AppBarNavigationClicked:
			l.tx <- frost.ToggleNavigation{}
		}
	}

	for {
		select {
		case <-l.rx: