Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI
This commit is contained in:
1822 files changed
+24568
-21314
No files matched your search
@@ -0,0 +1,279 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/data"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/input"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/internal"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/theme"
|
||||
)
|
||||
|
||||
// FlameNode is a profile frame; Value includes children.
|
||||
type FlameNode struct {
|
||||
Name string
|
||||
Value float64
|
||||
Children []FlameNode
|
||||
}
|
||||
|
||||
// FlamegraphState holds zoom and focus paths and the search.
|
||||
type FlamegraphState struct {
|
||||
Zoom, Focus []int
|
||||
Query string
|
||||
said string
|
||||
}
|
||||
|
||||
// FlamegraphOptions configure a Flamegraph.
|
||||
type FlamegraphOptions struct {
|
||||
// Unit follows values in the readout, as "ms".
|
||||
Unit string
|
||||
// Status shows loading or failure in place of the graph.
|
||||
Status data.DataStatus
|
||||
Error string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// flameRow is a frame row's height, a full hit target.
|
||||
const flameRow = theme.MinHitSize
|
||||
|
||||
// FlamegraphPalette colors frames at 3:1 on Surface.
|
||||
func FlamegraphPalette(k theme.Tokens) []ui.Color {
|
||||
// Ornament alone is too faint on ivory.
|
||||
return []ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Ornament.Mix(k.Text, 0.2), k.TextMuted}
|
||||
}
|
||||
|
||||
// Flamegraph draws a zoomable, searchable profile.
|
||||
func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts FlamegraphOptions) core.ChoiceResult {
|
||||
if s == nil {
|
||||
panic("mujica: Flamegraph needs a state")
|
||||
}
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
var res core.ChoiceResult
|
||||
res.Element = CodePanel(c).Disabled(opts.Disabled)
|
||||
res.Element.Children(func() {
|
||||
CodeHeader(c, func() {
|
||||
input.SearchInput(c, &s.Query, input.SearchInputOptions{Placeholder: core.Msg(c, "flamegraph.search"), Disabled: opts.Disabled}).Element.Grow(1)
|
||||
})
|
||||
if root == nil || opts.Status != data.DataReady {
|
||||
ui.Column(c).Height(flameRow * 6).Children(func() {
|
||||
res = core.NewChoiceResult(res.Element, data.DataPlaceholder(c, opts.Status, opts.Error, core.Msg(c, "flamegraph.empty"), core.Msg(c, "flamegraph.empty")))
|
||||
})
|
||||
return
|
||||
}
|
||||
flameCheck(root)
|
||||
if flameAt(root, s.Zoom) == nil {
|
||||
s.Zoom = nil
|
||||
}
|
||||
if flameAt(root, s.Focus) == nil {
|
||||
s.Focus = nil
|
||||
}
|
||||
q := strings.ToLower(strings.TrimSpace(s.Query))
|
||||
depth := flameDepth(root)
|
||||
g := ui.Box(c).Height(float32(depth)*flameRow).Shrink(0).Margin(d.Unit()*2, d.Unit()*3, 0, d.Unit()*3).Focusable().Label(core.Msg(c, "flamegraph.label"))
|
||||
internal.Ring(g, k.Focus, theme.ControlRadius)
|
||||
hover := ui.Local(g, "flamegraph.hover", func() []int { return nil })
|
||||
zoom, focus := s.Zoom, s.Focus
|
||||
if !g.IsDisabled() {
|
||||
if flameInput(c, g, root, s, hover) {
|
||||
res = core.NewChoiceResult(res.Element, true)
|
||||
}
|
||||
}
|
||||
b := g.Bounds()
|
||||
g.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if r.W != b.W {
|
||||
c.Invalidate()
|
||||
}
|
||||
flamePaint(p, r, k, root, zoom, focus, q, g.Focused(), core.FontSize(c, theme.CaptionSize))
|
||||
})
|
||||
readout := s.Focus
|
||||
if *hover != nil {
|
||||
readout = *hover
|
||||
}
|
||||
text := flameReadout(c, root, readout, opts.Unit)
|
||||
ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*3).
|
||||
Role(ui.RoleStatus).Shrink(0).SingleLine()
|
||||
if g.Focused() && text != s.said {
|
||||
s.said = text
|
||||
c.Announce(text)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
// flameInput zooms, moves focus and tracks hover.
|
||||
func flameInput(c *ui.Context, g *ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool {
|
||||
b := g.Bounds()
|
||||
x, y, over := g.PointerPosition()
|
||||
*hover = nil
|
||||
if over && b.W > 0 {
|
||||
*hover = flameHit(root, s.Zoom, x/b.W, int(y/flameRow))
|
||||
}
|
||||
changed := false
|
||||
zoomTo := func(path []int) {
|
||||
s.Zoom, s.Focus, changed = slices.Clone(path), slices.Clone(path), true
|
||||
slog.Debug("mujica: flamegraph zoom", "depth", len(path))
|
||||
}
|
||||
if g.Clicked() && *hover != nil {
|
||||
zoomTo(*hover)
|
||||
}
|
||||
f := s.Focus
|
||||
switch {
|
||||
case g.Shortcut(0, ui.KeyEnter):
|
||||
zoomTo(f)
|
||||
case g.Shortcut(0, ui.KeyEscape) && len(s.Zoom) > 0:
|
||||
zoomTo(s.Zoom[:len(s.Zoom)-1])
|
||||
case g.Shortcut(0, ui.KeyUp) && len(f) > 0:
|
||||
s.Focus, changed = f[:len(f)-1], true
|
||||
case g.Shortcut(0, ui.KeyDown):
|
||||
if n := flameAt(root, f); len(n.Children) > 0 {
|
||||
s.Focus, changed = append(slices.Clone(f), 0), true
|
||||
}
|
||||
case g.Shortcut(0, ui.KeyLeft) && len(f) > 0 && f[len(f)-1] > 0:
|
||||
s.Focus, changed = append(slices.Clone(f[:len(f)-1]), f[len(f)-1]-1), true
|
||||
case g.Shortcut(0, ui.KeyRight) && len(f) > 0 && f[len(f)-1] < len(flameAt(root, f[:len(f)-1]).Children)-1:
|
||||
s.Focus, changed = append(slices.Clone(f[:len(f)-1]), f[len(f)-1]+1), true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// flameCheck panics on impossible values.
|
||||
func flameCheck(n *FlameNode) {
|
||||
sum := 0.0
|
||||
for i := range n.Children {
|
||||
flameCheck(&n.Children[i])
|
||||
sum += n.Children[i].Value
|
||||
}
|
||||
if n.Value < 0 || sum > n.Value*(1+1e-9) {
|
||||
panic("mujica: FlameNode " + n.Name + " has a negative value or children larger than itself")
|
||||
}
|
||||
}
|
||||
|
||||
func flameAt(root *FlameNode, path []int) *FlameNode {
|
||||
n := root
|
||||
for _, i := range path {
|
||||
if i < 0 || i >= len(n.Children) {
|
||||
return nil
|
||||
}
|
||||
n = &n.Children[i]
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func flameDepth(n *FlameNode) int {
|
||||
d := 0
|
||||
for i := range n.Children {
|
||||
d = max(d, flameDepth(&n.Children[i]))
|
||||
}
|
||||
return d + 1
|
||||
}
|
||||
|
||||
// flameSpan returns a frame's fractional left and width.
|
||||
func flameSpan(root *FlameNode, zoom, path []int) (x, w float64) {
|
||||
if len(path) <= len(zoom) {
|
||||
return 0, 1
|
||||
}
|
||||
z := flameAt(root, zoom)
|
||||
x, w = 0, 1
|
||||
n := z
|
||||
for _, i := range path[len(zoom):] {
|
||||
for j := range i {
|
||||
x += w * n.Children[j].Value / max(n.Value, 1e-12)
|
||||
}
|
||||
w *= n.Children[i].Value / max(n.Value, 1e-12)
|
||||
n = &n.Children[i]
|
||||
}
|
||||
return x, w
|
||||
}
|
||||
|
||||
// flameHit finds the frame at fx in row.
|
||||
func flameHit(root *FlameNode, zoom []int, fx float32, row int) []int {
|
||||
if row < len(zoom) {
|
||||
return slices.Clone(zoom[:row])
|
||||
}
|
||||
path := slices.Clone(zoom)
|
||||
for len(path) < row {
|
||||
n := flameAt(root, path)
|
||||
next := -1
|
||||
for i := range n.Children {
|
||||
x, w := flameSpan(root, zoom, append(path, i))
|
||||
if float64(fx) >= x && float64(fx) < x+w {
|
||||
next = i
|
||||
}
|
||||
}
|
||||
if next < 0 {
|
||||
return nil
|
||||
}
|
||||
path = append(path, next)
|
||||
}
|
||||
if x, w := flameSpan(root, zoom, path); float64(fx) < x || float64(fx) >= x+w {
|
||||
return nil
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func flameColor(k theme.Tokens, name string) ui.Color {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(name))
|
||||
pal := FlamegraphPalette(k)
|
||||
return pal[h.Sum32()%uint32(len(pal))]
|
||||
}
|
||||
|
||||
// flamePaint draws visible frames with search and focus marks.
|
||||
func flamePaint(p *ui.Painter, r ui.Rect, k theme.Tokens, root *FlameNode, zoom, focus []int, q string, focused bool, size float32) {
|
||||
var walk func(n *FlameNode, path []int)
|
||||
walk = func(n *FlameNode, path []int) {
|
||||
fx, fw := flameSpan(root, zoom, path)
|
||||
w := float32(fw) * r.W
|
||||
if w < 1 {
|
||||
return
|
||||
}
|
||||
box := ui.Rect{X: internal.Snap(p, r.X+float32(fx)*r.W), Y: r.Y + float32(len(path))*flameRow, W: internal.Snap(p, w) - 1, H: flameRow - 2}
|
||||
col := flameColor(k, n.Name)
|
||||
face := col.Mix(k.Surface, 0.78)
|
||||
match := q != "" && strings.Contains(strings.ToLower(n.Name), q)
|
||||
above := len(path) < len(zoom)
|
||||
if q != "" && !match || above {
|
||||
face, col = k.Surface, k.Border
|
||||
}
|
||||
p.Fill(box, face, 2)
|
||||
p.Stroke(box, col, 2, 1)
|
||||
if match {
|
||||
p.Stroke(box, k.AccentText, 2, 2)
|
||||
}
|
||||
if focused && slices.Equal(path, focus) {
|
||||
internal.FocusRing(p, box, 2, k.Focus)
|
||||
}
|
||||
if box.W > 24 {
|
||||
fg := k.Text
|
||||
if above || q != "" && !match {
|
||||
fg = k.TextMuted
|
||||
}
|
||||
p.Clip(box, 2, func() { p.Text(box.X+6, box.Y+(box.H-size*1.3)/2, n.Name, size, fg) })
|
||||
}
|
||||
for i := range n.Children {
|
||||
if len(path) < len(zoom) && i != zoom[len(path)] {
|
||||
continue
|
||||
}
|
||||
walk(&n.Children[i], append(slices.Clone(path), i))
|
||||
}
|
||||
}
|
||||
walk(root, nil)
|
||||
}
|
||||
|
||||
// flameReadout names a frame's time and share.
|
||||
func flameReadout(c *ui.Context, root *FlameNode, path []int, unit string) string {
|
||||
n := flameAt(root, path)
|
||||
if n == nil {
|
||||
return core.Msg(c, "flamegraph.hint")
|
||||
}
|
||||
share := 100 * n.Value / max(root.Value, 1e-12)
|
||||
return fmt.Sprintf(core.Msg(c, "flamegraph.readout"), n.Name, n.Value, unit, share)
|
||||
}
|
||||
Reference in new issue
Block a user