193 lines
5.2 KiB
Go
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)
|
|
}
|