~f4814n/frost

f47a5a3f29d4638950e72a28ce5039a1147e5755 — Fabian Geiselhart 1 year, 3 months ago b5ab5be
Make OverviewPage.Layout nicer
2 files changed, 247 insertions(+), 240 deletions(-)

M main.go
A overview_page.go
M main.go => main.go +0 -240
@@ 1,20 1,13 @@
package main

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

	"gioui.org/app"
	"gioui.org/font/gofont"
	"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/material"
	"git.sr.ht/~f4814n/matrix"


@@ 107,239 100,6 @@ func (a *App) Layout(gtx Gtx) {
	a.page.Layout(gtx)
}

type InvalidationEvent struct{}

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() {
		c := make(chan matrix.Event, 100)
		o.cli.Notify(c)
		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
	}

	w.rooms = moveToFront(roomListElement{room: room, lastEvent: event}, 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 overview_page.go => overview_page.go +247 -0
@@ 0,0 1,247 @@
package main

import (
	"context"
	"fmt"
	"image"
	"unicode"

	"gioui.org/gesture"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"git.sr.ht/~f4814n/matrix"
	log "github.com/sirupsen/logrus"
)

type InvalidationEvent struct{}

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() {
		c := make(chan matrix.Event, 100)
		o.cli.Notify(c)
		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 {
				log.Infof("selected room: %s", room.room.ID)
				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
}

// NewEvent updates the room list when a new event arrives
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
	}

	for i, elem := range w.rooms {
		if elem.room == room {
			w.rooms = append(w.rooms[:i], w.rooms[i+1:]...)
			break
		}
	}
	w.rooms = append([]roomListElement{roomListElement{room:room, lastEvent: event}}, w.rooms...)
}

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

// 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)
	})
}

// roomListElement is a element in the room list
type roomListElement struct {
	click     gesture.Click
	room      matrix.Room
	lastEvent matrix.Event
}

func (w *roomListElement) Layout(gtx Gtx) Dims {
	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(w.layoutIcon),
				layout.Rigid(func(gtx Gtx) Dims {
					return column().Layout(gtx,
						layout.Rigid(w.layoutUpper),
						layout.Rigid(w.layoutLastEvent),
					)
				}),
			)
		})
		pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
		w.click.Add(gtx.Ops)
		return dims
	})
}

func (w *roomListElement) layoutIcon(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)
}

func (w *roomListElement) layoutUpper(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	return baseline().Layout(gtx,
		layout.Rigid(func(gtx Gtx) Dims {
			return material.H6(theme, w.room.Displayname()).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
				return lbl.Layout(gtx)
			})
		}),
	)
}

func (w *roomListElement) layoutLastEvent(gtx Gtx) Dims {
	bgtextcol := rgb(0xbbbbbb)
	in := layout.Inset{Top: unit.Dp(6)}
	return in.Layout(gtx, func(gtx Gtx) Dims {
		lbl := material.Body2(theme, formatEvent(w.lastEvent))
		lbl.Color = bgtextcol
		lbl.MaxLines = 1
		return lbl.Layout(gtx)
	})
}

// 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 formatEvent(event matrix.Event) string {
	switch event := event.(type) {
	case matrix.RoomEvent:
		switch event.Type {
		case "m.room.message":
			if event.Content["msgtype"] == "m.text" {
				return fmt.Sprintf("%s: %s", event.Sender.Displayname(), event.Content["body"])
			}
			return fmt.Sprintf("Unsupported message type: %s", event.Content["msgtype"])
		}
	case matrix.StateEvent:
	}

	return fmt.Sprintf("%#v", event)
}