Files
MujicaUI/workload_view.go
T

127 lines
4.3 KiB
Go

package mujica
import (
"fmt"
"math"
"strconv"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/theme"
)
// Workload is one member's assigned and available hours.
type Workload struct {
Member ProjectMember
Assigned float64
Capacity float64
}
// WorkloadViewOptions configure a WorkloadView.
type WorkloadViewOptions struct {
// Unit labels the amounts; empty uses hours.
Unit string
Label string
// Version changes when loads change in place.
Version int
}
// WorkloadView virtualizes member loads with capacity and overload marks.
func WorkloadView(c *ui.Context, loads []Workload, opts WorkloadViewOptions) *ui.Element {
k := Tokens(c)
d := Density(c)
unit := opts.Unit
if unit == "" {
unit = msg(c, "workload_view.hours")
}
label := opts.Label
if label == "" {
label = msg(c, "workload_view.label")
}
root := ui.Column(c).Grow(1).MinHeight(0).Role(ui.RoleList).Label(label)
cache := ui.Local(root, "workload-summary", func() workloadCache { return workloadCache{} })
var first *Workload
if len(loads) > 0 {
first = &loads[0]
}
if !cache.ready || cache.first != first || cache.n != len(loads) || cache.version != opts.Version {
*cache = workloadSummary(loads)
cache.first, cache.n, cache.version, cache.ready = first, len(loads), opts.Version, true
}
top := cache.top
state := ui.Local(root, "workload-list", func() *ui.ListState { return &ui.ListState{} })
st := *state
st.Key = func(i int) any { return loads[i].Member.ID }
num := func(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) }
root.Children(func() {
over := cache.over
if over > 0 {
ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(k.Danger).Role(ui.RoleStatus).Padding(d.Unit()).Children(func() {
ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14)
ui.Text(c, calendarCount(c, "workload_view.overloaded", over)).FontSize(FontSize(c, theme.CaptionSize))
})
}
ui.List(c, st, len(loads), func(i int) {
l := loads[i]
hot := l.Assigned > l.Capacity
text := fmt.Sprintf("%s / %s %s", num(l.Assigned), num(l.Capacity), unit)
note := ""
if hot {
note = fmt.Sprintf(msg(c, "workload_view.over"), num(l.Assigned-l.Capacity), unit)
}
ui.Row(c).MinHeight(d.ControlHeight()).Gap(d.Unit() * 3).AlignItems(ui.Center).PaddingX(d.Unit()).Role(ui.RoleListItem).
Label(l.Member.Name + ", " + text + " " + note).Children(func() {
Avatar(c, l.Member.Name, AvatarOptions{Size: 24})
ui.Text(c, l.Member.Name).Width(d.ControlHeight() * 4).Shrink(0).SingleLine().Ellipsis("…").TextColor(k.Text)
ui.Box(c).Grow(1).Height(10).Draw(func(p *ui.Painter, r ui.Rect) { workloadBar(p, r, l, top, k.Border, k.AccentText, k.Danger, k.Text) })
fg := k.TextMuted
if hot {
fg = k.Danger
ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14).TextColor(fg)
}
ui.Text(c, text).Width(d.ControlHeight() * 3).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).TextColor(fg).FontFeatures("tnum").TextAlign(ui.End)
})
}).Grow(1).MinHeight(d.ControlHeight() * 3)
})
return root
}
// workloadBar draws assigned work, overload hatching and capacity marks.
func workloadBar(p *ui.Painter, r ui.Rect, l Workload, top float64, track, fill, hot, tick ui.Color) {
x := func(v float64) float32 { return r.X + r.W*float32(v/top) }
p.Fill(r, track, 3)
p.Fill(ui.Rect{X: r.X, Y: r.Y, W: x(min(l.Assigned, l.Capacity)) - r.X, H: r.H}, fill, 3)
if l.Assigned > l.Capacity {
p.Fill(ui.Rect{X: x(l.Capacity), Y: r.Y, W: x(l.Assigned) - x(l.Capacity), H: r.H}, hot, 0)
for sx := x(l.Capacity) + 2; sx < x(l.Assigned); sx += 5 {
p.Line(sx, r.Y+r.H, sx+3, r.Y, 1, track)
}
}
cx := x(l.Capacity)
p.Line(cx, r.Y-3, cx, r.Y+r.H+3, 2, tick)
}
type workloadCache struct {
first *Workload
n, version, over int
top float64
ready bool
}
func workloadSummary(loads []Workload) workloadCache {
out := workloadCache{}
seen := map[string]bool{}
for _, l := range loads {
if l.Member.ID == "" || seen[l.Member.ID] || l.Assigned < 0 || l.Capacity <= 0 || math.IsNaN(l.Assigned) || math.IsNaN(l.Capacity) || math.IsInf(l.Assigned, 0) || math.IsInf(l.Capacity, 0) {
panic("mujica: WorkloadView needs unique member IDs and finite non-negative loads with positive capacity")
}
seen[l.Member.ID] = true
out.top = max(out.top, l.Assigned, l.Capacity)
if l.Assigned > l.Capacity {
out.over++
}
}
return out
}