Files

245 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package events
import (
"slices"
"strconv"
"strings"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/services"
"aego/editor/core/state"
"aego/engine"
"aego/engine/recorder"
"aego/scene"
"aego/ui/core"
"aego/ui/widgets"
)
const ID = "events"
var shown = [...]recorder.Kind{recorder.Signal, recorder.Event, recorder.Fault, recorder.Spawn, recorder.Despawn, recorder.SceneChange, recorder.Input}
type Deps struct {
World func() *engine.World
Selection services.Selection
Notices services.Notices
Root string
}
type Panel struct {
d Deps
recs []recorder.Record
view []int
rev uint64
src *recorder.Recorder
show [recorder.KindCount]bool
search string
frame uint64
selected bool
replayed bool
brk bool
brkText string
scroll float32
sel int
lastSel []ids.NodeID
}
func New(d Deps) *Panel {
p := &Panel{d: d, sel: -1}
for i := range p.show {
p.show[i] = true
}
return p
}
func (p *Panel) ID() string {
return ID
}
func (p *Panel) Title() string {
return "Event Monitor"
}
func (p *Panel) Focus(frame uint64) {
p.frame = frame
p.scroll = 0
}
func (p *Panel) source() (*recorder.Recorder, *scene.Scene) {
if p.d.World == nil {
return nil, nil
}
w := p.d.World()
if w == nil || w.Recorder == nil {
return nil, nil
}
s, _ := engine.ServiceOf[*scene.Scene](w, scene.ServiceID)
return w.Recorder, s
}
func (p *Panel) mask() uint32 {
var m uint32
for i, on := range p.show {
if on {
m |= recorder.Bit(recorder.Kind(i))
}
}
return m
}
func (p *Panel) apply(rec *recorder.Recorder) {
if p.brk {
rec.BreakOn(p.mask(), p.brkText)
} else {
rec.BreakOn(0, "")
}
var nodes []ids.NodeID
if p.selected {
nodes = p.d.Selection.Selected()
}
if rec != p.src || !slices.Equal(nodes, p.lastSel) {
rec.SetFilter(recorder.All, nodes)
p.lastSel = slices.Clone(nodes)
}
}
func (p *Panel) Check() (string, bool) {
rec, _ := p.source()
if rec == nil || !p.brk {
return "", false
}
p.apply(rec)
r, ok := rec.Tripped()
if !ok {
return "", false
}
return r.Kind.String() + " " + r.Name, true
}
func nameOf(s *scene.Scene) func(ids.NodeID) string {
return func(id ids.NodeID) string {
if s != nil {
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) {
return s.Name(h)
}
}
return id.String()[:8]
}
}
func (p *Panel) refresh(rec *recorder.Recorder, s *scene.Scene) {
if rec != p.src || rec.Revision() != p.rev {
p.recs = rec.Snapshot(p.recs)
p.rev = rec.Revision()
}
p.src = rec
q := strings.ToLower(strings.TrimSpace(p.search))
name := nameOf(s)
p.view = p.view[:0]
for i := range p.recs {
r := &p.recs[i]
if !p.show[r.Kind] || (r.Replayed && !p.replayed) || (p.frame != 0 && r.Frame != p.frame) {
continue
}
if q != "" && !strings.Contains(strings.ToLower(r.Line(name)), q) {
continue
}
p.view = append(p.view, i)
}
}
func (p *Panel) export(s *scene.Scene) {
var b strings.Builder
name := nameOf(s)
for _, i := range p.view {
b.WriteString(p.recs[i].Line(name))
b.WriteByte('\n')
}
path, err := state.WriteEvents(p.d.Root, []byte(b.String()))
if err != nil {
p.d.Notices.Notify(context.LevelError, "cannot write the event log: "+err.Error())
return
}
p.d.Notices.Notify(context.LevelInfo, "Event log written to "+path)
}
func (p *Panel) toolbar(ui *core.Context, s *scene.Scene) {
row := ui.Cursor().Next(0)
x := row.X
for i, k := range shown {
w := ui.TextSize(k.String()).X + 16
widgets.ToggleButton(ui, core.IDSeq(ui.ID("kind"), i), mathx.Rect{X: x, Y: row.Y, W: w, H: row.H}, k.String(), &p.show[k])
x += w + 4
}
if row.X+row.W-x > 80 {
widgets.SearchField(ui, ui.ID("search"), mathx.Rect{X: x, Y: row.Y, W: row.X + row.W - x, H: row.H}, &p.search)
}
row = ui.Cursor().Next(0)
x = row.X
place := func(w float32) mathx.Rect {
r := mathx.Rect{X: x, Y: row.Y, W: w, H: row.H}
x += w + 4
return r
}
widgets.ToggleButton(ui, ui.ID("selected"), place(130), "Selected nodes", &p.selected)
widgets.ToggleButton(ui, ui.ID("replayed"), place(130), "Show replayed", &p.replayed)
if p.frame != 0 && widgets.ButtonRect(ui, ui.ID("frame"), place(130), "frame "+strconv.FormatUint(p.frame, 10)+" ×") {
p.frame = 0
}
if widgets.ButtonRect(ui, ui.ID("export"), place(70), "Export") {
p.export(s)
}
widgets.ToggleButton(ui, ui.ID("break"), place(110), "Break on", &p.brk)
if row.X+row.W-x > 40 {
widgets.TextFieldRect(ui, ui.ID("breaktext"), mathx.Rect{X: x, Y: row.Y, W: row.X + row.W - x, H: row.H}, &p.brkText, true)
}
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
ui := f.UI
th := &ui.Theme
rec, s := p.source()
if rec == nil {
widgets.LabelDim(ui, "Events of the running game appear here while Play runs")
return
}
p.apply(rec)
p.refresh(rec, s)
p.toolbar(ui, s)
counts := rec.Counts()
var c strings.Builder
for _, k := range shown {
c.WriteString(k.String() + " " + strconv.FormatUint(counts[k], 10) + " ")
}
widgets.LabelDim(ui, c.String())
top := ui.Cursor().Y
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
name := nameOf(s)
widgets.VirtualList(ui, ui.ID("list"), list, len(p.view), th.RowHeight, &p.scroll, func(i int, row mathx.Rect) {
idx := p.view[i]
rc := &p.recs[idx]
id := core.IDSeq(ui.ID("row"), idx)
if ui.Pressed(id, row) {
p.sel = idx
if !rc.Owner.IsZero() {
p.d.Selection.Select(rc.Owner)
}
}
switch {
case p.sel == idx:
ui.Rect(row, th.SelectionDim)
case ui.Hot() == id:
ui.Rect(row, th.WidgetHot)
}
col := th.Text
if rc.Kind == recorder.Fault {
col = th.Error
}
ui.PushClip(row)
ui.Text(rc.Line(name), mathx.Vec2{X: row.X + 4, Y: ui.TextY(row)}, col)
ui.PopClip()
})
}