Files
MujicaUI/member_list.go
T

193 lines
5.2 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/theme"
)
// Member supplies a unique ID, name, presence and role.
type Member struct {
ID, Name string
Image *ui.Bitmap
Presence Presence
// Role, as "Owner"; empty shows none.
Role string
}
// MemberListOptions configure a MemberList.
type MemberListOptions struct {
// Height is the list's height; zero means 360.
Height float32
Disabled bool
}
// MemberListState keeps a MemberList's place and choice.
type MemberListState struct {
rows ui.ListState
cursor int
// Selected is the chosen member's ID; empty is none.
Selected string
cache memberRows
looked string
lookedGen int
}
type memberRows struct {
first *Member
n int
rows []int
online int
offline int
gen int
}
// MemberListResult is a MemberList's element and this frame's events.
type MemberListResult struct {
Element *ui.Element
changed bool
// Opened contains the ID activated by Enter or double-click.
Opened string
}
// Changed reports a new selection.
func (r MemberListResult) Changed() bool { return r.changed }
// MemberList virtualizes online and offline groups.
func MemberList(c *ui.Context, s *MemberListState, members []Member, opts MemberListOptions) MemberListResult {
if opts.Height < 0 {
panic("mujica: MemberList Height must not be negative")
}
h := opts.Height
if h == 0 {
h = 360
}
memberGroup(s, members)
k := Tokens(c)
d := Density(c)
rows := s.cache.rows
if s.looked != s.Selected || s.lookedGen != s.cache.gen {
s.looked, s.lookedGen, s.cursor = s.Selected, s.cache.gen, -1
for i, r := range rows {
if s.Selected != "" && r >= 0 && members[r].ID == s.Selected {
s.cursor = i
break
}
}
}
s.rows.Selected = &s.cursor
s.rows.Header = func(i int) bool { return rows[i] < 0 }
s.rows.Key = func(i int) any {
if rows[i] < 0 {
return rows[i]
}
return members[rows[i]].ID
}
s.rows.Label = func(i int) string {
if rows[i] < 0 {
return memberHeader(c, s, rows[i])
}
return members[rows[i]].Name
}
var r MemberListResult
box := ui.Column(c).Height(h).Disabled(opts.Disabled)
r.Element = box
box.Children(func() {
if len(rows) == 0 {
ui.Text(c, msg(c, "member_list.empty")).TextColor(k.TextMuted)
return
}
restore := dataSelectTheme(c, 0)
l := ui.List(c, &s.rows, len(rows), func(i int) {
if rows[i] < 0 {
ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).AlignItems(ui.Center).Background(k.Surface).Children(func() {
ui.Text(c, memberHeader(c, s, rows[i])).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.TextMuted)
})
return
}
m := members[rows[i]]
ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
ui.Box(c).Shrink(0).Children(func() {
Avatar(c, m.Name, AvatarOptions{Image: m.Image, Size: 28})
ui.Box(c).Absolute().Right(-2).Bottom(-2).Padding(1.5).Radius(6).Background(k.Surface).Children(func() {
OnlineStatus(c, m.Presence, OnlineStatusOptions{HideText: true, Size: 9})
})
})
t := ui.Text(c, m.Name).SingleLine().Grow(1).MinWidth(0)
if m.Presence == PresenceOffline {
t.TextColor(k.TextMuted)
}
if m.Role != "" {
Badge(c, BadgeOptions{Text: m.Role, Tone: BadgeNeutral}, nil)
}
if i == s.cursor {
ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText)
}
})
})
restore()
l.Grow(1).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Padding(d.Unit())
if l.IsDisabled() {
return
}
if l.Changed() && s.cursor >= 0 && rows[s.cursor] >= 0 {
s.Selected, r.changed = members[rows[s.cursor]].ID, true
s.looked = s.Selected
}
if l.Submitted() && s.cursor >= 0 && rows[s.cursor] >= 0 {
r.Opened = members[rows[s.cursor]].ID
}
})
if r.changed || r.Opened != "" {
slog.Debug("mujica: member_list choose", "id", s.Selected, "opened", r.Opened)
}
return r
}
func memberHeader(c *ui.Context, s *MemberListState, row int) string {
if row == -1 {
return fmt.Sprintf(msg(c, "member_list.online"), s.cache.online)
}
return fmt.Sprintf(msg(c, "member_list.offline"), s.cache.offline)
}
// memberGroup rebuilds the sections only when members changed.
func memberGroup(s *MemberListState, members []Member) {
var first *Member
if len(members) > 0 {
first = &members[0]
}
if s.cache.rows != nil && s.cache.first == first && s.cache.n == len(members) {
return
}
seen := make(map[string]bool, len(members))
on, off := []int{-1}, []int{-2}
for i, m := range members {
if m.ID == "" || m.Name == "" || seen[m.ID] {
panic("mujica: MemberList members need unique IDs and names: " + m.ID)
}
seen[m.ID] = true
m.Presence.key()
if m.Presence == PresenceOffline {
off = append(off, i)
} else {
on = append(on, i)
}
}
s.cache = memberRows{first: first, n: len(members), online: len(on) - 1, offline: len(off) - 1, gen: s.cache.gen + 1}
if len(on) > 1 {
s.cache.rows = append(s.cache.rows, on...)
}
if len(off) > 1 {
s.cache.rows = append(s.cache.rows, off...)
}
if s.cache.rows == nil {
s.cache.rows = []int{}
}
slog.Debug("mujica: member_list grouped", "online", s.cache.online, "offline", s.cache.offline)
}