Files

634 lines
16 KiB
Go

package inspector
import (
"strconv"
"aego/core/curve"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/document"
"aego/editor/core/history"
"aego/editor/core/props"
"aego/platform/input"
"aego/schema"
"aego/ui/core"
"aego/ui/widgets"
)
var all = widgets.Edit{Changed: true, Begin: true, End: true}
type fieldRow struct {
a, b int
}
func visibleField(fd *schema.FieldDescriptor, play bool) bool {
return fd.Flags.Has(schema.FlagEditor) && !fd.Hints.Hidden && (!fd.Hints.Runtime || play)
}
func pairGroup(fd *schema.FieldDescriptor) int {
if len(fd.Hints.Flags) > 0 {
return 0
}
switch fd.Kind {
case schema.KindBool:
return 1
case schema.KindInt32, schema.KindInt64, schema.KindFloat32, schema.KindFloat64:
return 2
}
return 0
}
func plan(td *schema.TypeDescriptor, play bool) []fieldRow {
var rows []fieldRow
pending := -1
for i := range td.Fields {
fd := &td.Fields[i]
if !visibleField(fd, play) {
continue
}
if pending >= 0 {
prev := &td.Fields[pending]
if g := pairGroup(fd); g != 0 && g == pairGroup(prev) && prev.Hints.Category == fd.Hints.Category {
rows = append(rows, fieldRow{a: pending, b: i})
pending = -1
continue
}
rows = append(rows, fieldRow{a: pending, b: -1})
pending = -1
}
if pairGroup(fd) != 0 {
pending = i
continue
}
rows = append(rows, fieldRow{a: i, b: -1})
}
if pending >= 0 {
rows = append(rows, fieldRow{a: pending, b: -1})
}
return rows
}
func pairRects(ui *core.Context, r mathx.Rect, secondLabel string) (*[2]mathx.Rect, *[2]mathx.Rect) {
lw := widgets.LabelWidth(r.W)
vx, vw := r.X+lw, r.W-lw
const gap = float32(8)
half := (vw - gap) / 2
first := &[2]mathx.Rect{
{X: r.X, Y: r.Y, W: lw - 4, H: r.H},
{X: vx, Y: r.Y, W: half, H: r.H},
}
x2 := vx + half + gap
l2 := min(ui.TextSize(secondLabel).X+10, half*0.5)
second := &[2]mathx.Rect{
{X: x2, Y: r.Y, W: l2 - 4, H: r.H},
{X: x2 + l2, Y: r.Y, W: half - l2, H: r.H},
}
return first, second
}
func (p *Panel) fields(d *document.SceneDocument, td *schema.TypeDescriptor) {
ui := p.f.UI
wide := ui.Cursor().W >= 300
category := ""
for _, row := range plan(td, p.d.Docs.Documents().InPlay()) {
fd := &td.Fields[row.a]
if c := fd.Hints.Category; c != "" && c != category {
category = c
widgets.LabelDim(ui, c)
}
if row.b < 0 || !wide {
p.field(d, td, fd)
if row.b >= 0 {
p.field(d, td, &td.Fields[row.b])
}
continue
}
second := &td.Fields[row.b]
r := ui.Cursor().Next(0)
a, b := pairRects(ui, r, p.labelOf(second))
p.fieldIn(d, td, fd, a)
p.fieldIn(d, td, second, b)
}
}
func (p *Panel) field(d *document.SceneDocument, td *schema.TypeDescriptor, fd *schema.FieldDescriptor) {
p.fieldIn(d, td, fd, nil)
}
func (p *Panel) fieldIn(d *document.SceneDocument, td *schema.TypeDescriptor, fd *schema.FieldDescriptor, at *[2]mathx.Rect) {
ui := p.f.UI
ui.PushRawID(core.ID(fd.ID))
defer ui.PopID()
key := ui.ID("value")
v, mixed, err := d.Props.Common(p.nodes, td.ID, fd.ID)
if err != nil {
return
}
if cached, ok := p.live[key]; ok {
if p.editing(ui, key) {
v, mixed = cached, false
} else {
delete(p.live, key)
}
}
tip := fd.Hints.Tooltip
if mixed {
tip = p.mixedTip(d, td, fd, tip)
}
cur := ui.Cursor()
rowTop := cur.Y
pre := mathx.Rect{X: cur.X, Y: cur.Y, W: cur.W, H: ui.Theme.RowHeight}
if at != nil {
pre = mathx.Rect{X: at[0].X, Y: at[0].Y, W: at[1].X + at[1].W - at[0].X, H: at[0].H}
}
jump := td.Kind == schema.TypeScript && ui.Primary() && ui.MousePressed(input.MouseLeft) && ui.Hit(pre) && !ui.MouseInOverlay()
ro := fd.Flags.Has(schema.FlagReadOnly)
var e widgets.Edit
var row mathx.Rect
if at != nil {
e = p.scalarIn(d, fd, &v, mixed, p.labelOf(fd), tip, ro, at[0], at[1])
row = pre
} else {
e = p.value(d, fd, &v, mixed, p.labelOf(fd), tip, ro)
row = mathx.Rect{X: cur.X, Y: rowTop, W: cur.W, H: max(0, cur.Y-rowTop)}
}
if p.overridden(td, fd) {
ui.Rect(mathx.Rect{X: row.X - 5, Y: row.Y + 1, W: 2, H: max(row.H-2, 2)}, ui.Theme.Accent)
}
if jump {
ui.ReleaseMouse()
ui.CancelFocus()
ui.CloseAllPopups()
delete(p.live, key)
p.gotoScript(td.FullName, fd.Name)
return
}
if ui.Hit(row) && !ui.MouseInOverlay() {
p.hoverField = fieldRef{t: td.ID, f: fd.ID, ok: true}
if ui.MousePressed(input.MouseRight) {
p.menuField = p.hoverField
p.rightUsed = true
widgets.OpenContextMenu(ui, p.fieldMenuID)
}
}
if e.Begin || e.Changed {
p.live[key] = v
}
if e.End {
delete(p.live, key)
}
if !e.Any() {
return
}
t, f, val := td.ID, fd.ID, v
p.apply(e, "Set "+props.Label(fd), func(tx *history.Transaction) error {
return d.Props.Set(tx, p.nodes, t, f, val)
})
}
func (p *Panel) scalarIn(d *document.SceneDocument, fd *schema.FieldDescriptor, v *schema.Value, mixed bool, label, tip string, ro bool, lr, r mathx.Rect) widgets.Edit {
ui := p.f.UI
th := &ui.Theme
ui.PushClip(lr)
ui.Text(label, mathx.Vec2{X: lr.X, Y: ui.TextY(lr)}, th.TextDim)
ui.PopClip()
lid := ui.ID("label")
ui.MouseOver(lid, lr)
widgets.Tooltip(ui, lid, tip)
if ro {
ui.PushClip(r)
ui.TextIn(r, p.summary(d, fd, *v, mixed), th.TextDim, core.AlignLeft)
ui.PopClip()
return widgets.Edit{}
}
return p.scalar(d, ui.ID("value"), r, fd, v, mixed)
}
func (p *Panel) value(d *document.SceneDocument, fd *schema.FieldDescriptor, v *schema.Value, mixed bool, label, tip string, ro bool) widgets.Edit {
switch fd.Kind {
case schema.KindList:
return p.listValue(d, fd, v, mixed, label, ro)
case schema.KindStruct:
return p.structValue(d, fd, v, mixed, label, ro)
}
ui := p.f.UI
lr, r := widgets.FieldRects(ui, label)
lid := ui.ID("label")
ui.MouseOver(lid, lr)
widgets.Tooltip(ui, lid, tip)
if ro {
ui.PushClip(r)
ui.TextIn(r, p.summary(d, fd, *v, mixed), ui.Theme.TextDim, core.AlignLeft)
ui.PopClip()
return widgets.Edit{}
}
return p.scalar(d, ui.ID("value"), r, fd, v, mixed)
}
func numOpts(fd *schema.FieldDescriptor, mixed, isInt bool) widgets.NumberOpts {
o := widgets.NumberOpts{
Mixed: mixed,
Int: isInt,
Min: fd.Min,
Max: fd.Max,
HasMin: fd.HasMin,
HasMax: fd.HasMax,
Speed: fd.Hints.Step,
}
o.Unit = fd.Hints.Unit
if o.Speed == 0 {
switch {
case fd.HasMin && fd.HasMax && fd.Max > fd.Min:
o.Speed = (fd.Max - fd.Min) / 200
case isInt:
o.Speed = 0.1
default:
o.Speed = 0.05
}
}
return o
}
func intValue(k schema.Kind, n int64) schema.Value {
if k == schema.KindInt32 {
return schema.Int32(int32(n))
}
return schema.Int64(n)
}
func floatValue(k schema.Kind, f float64) schema.Value {
if k == schema.KindFloat32 {
return schema.Float32(float32(f))
}
return schema.Float64(f)
}
func (p *Panel) scalar(d *document.SceneDocument, id core.ID, r mathx.Rect, fd *schema.FieldDescriptor, v *schema.Value, mixed bool) widgets.Edit {
ui := p.f.UI
th := &ui.Theme
switch fd.Kind {
case schema.KindBool:
b := v.I != 0
state := 0
if b {
state = 1
}
if mixed {
state = 2
}
if triCheck(ui, id, mathx.Rect{X: r.X, Y: r.Y, W: r.H, H: r.H}, state) {
*v = schema.Bool(!b || mixed)
return all
}
case schema.KindInt32, schema.KindInt64:
if fd.Hints.Choices.Section != "" {
return p.choices(id, r, fd, v, mixed)
}
if fd.Hints.Bitset.Section != "" {
return p.bitset(id, r, fd, v, mixed)
}
if len(fd.Hints.Flags) > 0 {
m := uint64(v.AsInt())
if widgets.MaskField(ui, id, r, &m, fd.Hints.Flags, mixed) {
*v = intValue(fd.Kind, int64(m))
return all
}
return widgets.Edit{}
}
f := float64(v.AsInt())
e := widgets.DragNumber(ui, id, r, &f, numOpts(fd, mixed, true))
if e.Changed {
*v = intValue(fd.Kind, int64(f))
}
return e
case schema.KindFloat32, schema.KindFloat64:
f := v.AsFloat()
e := widgets.DragNumber(ui, id, r, &f, numOpts(fd, mixed, false))
if e.Changed {
*v = floatValue(fd.Kind, f)
}
return e
case schema.KindVec2:
vec := v.V
e := widgets.Vec2Field(ui, id, r, &vec, numOpts(fd, false, false), mixed, mixed)
if e.Changed {
*v = schema.Vec2(vec)
}
return e
case schema.KindVec3:
vec := v.V3
e := widgets.Vec3Field(ui, id, r, &vec, numOpts(fd, false, false), [3]bool{mixed, mixed, mixed})
if e.Changed {
*v = schema.Vec3(vec)
}
return e
case schema.KindQuat:
eul := props.EulerDeg(v.Q)
o := numOpts(fd, false, false)
o.Speed, o.Prec, o.Suffix = 0.5, 2, "°"
e := widgets.Vec3Field(ui, id, r, &eul, o, [3]bool{mixed, mixed, mixed})
if e.Changed {
*v = schema.Quat(props.QuatFromEulerDeg(eul))
}
return e
case schema.KindVec4:
vec := v.V4
parts := [4]*float32{&vec.X, &vec.Y, &vec.Z, &vec.W}
w := (r.W - 6) / 4
var e widgets.Edit
for i, ptr := range parts {
cell := mathx.Rect{X: r.X + float32(i)*(w+2), Y: r.Y, W: w, H: r.H}
e = e.Or(widgets.DragFloat32(ui, core.IDSeq(id, i+1), cell, ptr, numOpts(fd, mixed, false)))
}
if e.Changed {
*v = schema.Vec4(vec)
}
return e
case schema.KindColor:
col := v.C
e := widgets.ColorField(ui, id, r, &col, mixed)
if e.Changed {
*v = schema.Color(col)
}
return e
case schema.KindRect:
rect := v.R
parts := [4]*float32{&rect.X, &rect.Y, &rect.W, &rect.H}
w := (r.W - 6) / 4
var e widgets.Edit
for i, ptr := range parts {
cell := mathx.Rect{X: r.X + float32(i)*(w+2), Y: r.Y, W: w, H: r.H}
e = e.Or(widgets.DragFloat32(ui, core.IDSeq(id, i+1), cell, ptr, numOpts(fd, mixed, false)))
}
if e.Changed {
*v = schema.Rect(rect)
}
return e
case schema.KindString:
s := v.S
if mixed {
s = th.Mixed
}
if _, committed := widgets.TextFieldRect(ui, id, r, &s, false); committed && !(mixed && s == th.Mixed) {
*v = schema.String(s)
return all
}
case schema.KindEnum:
if len(fd.Enum) == 0 {
f := float64(v.AsInt())
e := widgets.DragNumber(ui, id, r, &f, numOpts(fd, mixed, true))
if e.Changed {
*v = schema.Enum(int64(f))
}
return e
}
names := make([]string, len(fd.Enum))
cur := -1
for i, ev := range fd.Enum {
names[i] = ev.Name
if ev.Value == v.I {
cur = i
}
}
if widgets.Combo(ui, id, r, &cur, names, mixed) && cur >= 0 {
*v = schema.Enum(fd.Enum[cur].Value)
return all
}
case schema.KindCurve:
st := p.curves[id]
if st == nil {
st = widgets.NewCurveState()
p.curves[id] = st
}
cv := v.Cv
e := widgets.CurveField(ui, id, r, &cv, mixed, st)
if e.Changed {
*v = schema.Curve(cv)
}
return e
case schema.KindAssetRef:
return p.assetField(id, r, fd, v, mixed)
case schema.KindNodeRef:
return p.nodeField(d, id, r, v, mixed, fd.Component)
}
return widgets.Edit{}
}
func zeroValue(reg *schema.Registry, fd *schema.FieldDescriptor) schema.Value {
switch fd.Kind {
case schema.KindBool:
return schema.Bool(false)
case schema.KindInt32:
return schema.Int32(0)
case schema.KindInt64:
return schema.Int64(0)
case schema.KindFloat32:
return schema.Float32(0)
case schema.KindFloat64:
return schema.Float64(0)
case schema.KindVec2:
return schema.Vec2(mathx.Vec2{})
case schema.KindVec3:
return schema.Vec3(mathx.Vec3{})
case schema.KindVec4:
return schema.Vec4(mathx.Vec4{})
case schema.KindQuat:
return schema.Quat(mathx.QuatIdentity())
case schema.KindColor:
return schema.Color(mathx.Color{R: 1, G: 1, B: 1, A: 1})
case schema.KindRect:
return schema.Rect(mathx.Rect{})
case schema.KindCurve:
return schema.Curve(curve.Line(0, 0, 1, 1))
case schema.KindString:
return schema.String("")
case schema.KindEnum:
if len(fd.Enum) > 0 {
return schema.Enum(fd.Enum[0].Value)
}
return schema.Enum(0)
case schema.KindAssetRef:
return schema.Asset(ids.AssetID{})
case schema.KindNodeRef:
return schema.Node(ids.NodeID{})
case schema.KindList:
return schema.Value{Kind: schema.KindList}
case schema.KindStruct:
td, ok := reg.ByID(fd.Struct)
if !ok {
return schema.Value{Kind: schema.KindStruct}
}
obj := &schema.Object{Type: td.ID, Fields: make([]schema.Value, len(td.Fields))}
for i := range td.Fields {
obj.Fields[i] = zeroValue(reg, &td.Fields[i])
}
return schema.Value{Kind: schema.KindStruct, Obj: obj}
}
return schema.Value{}
}
func (p *Panel) summary(d *document.SceneDocument, fd *schema.FieldDescriptor, v schema.Value, mixed bool) string {
if mixed {
return p.f.UI.Theme.Mixed
}
num := func(f float32) string {
return widgets.FormatNumber(float64(f), widgets.NumberOpts{})
}
switch v.Kind {
case schema.KindBool:
return strconv.FormatBool(v.I != 0)
case schema.KindInt32, schema.KindInt64:
return strconv.FormatInt(v.I, 10)
case schema.KindFloat32, schema.KindFloat64:
return widgets.FormatNumber(v.F, widgets.NumberOpts{})
case schema.KindString:
return v.S
case schema.KindVec2:
return num(v.V.X) + ", " + num(v.V.Y)
case schema.KindVec3:
return num(v.V3.X) + ", " + num(v.V3.Y) + ", " + num(v.V3.Z)
case schema.KindVec4:
return num(v.V4.X) + ", " + num(v.V4.Y) + ", " + num(v.V4.Z) + ", " + num(v.V4.W)
case schema.KindQuat:
e := props.EulerDeg(v.Q)
return num(e.X) + "°, " + num(e.Y) + "°, " + num(e.Z) + "°"
case schema.KindColor:
return widgets.HexColor(v.C)
case schema.KindCurve:
if v.Cv.Ease != curve.None {
return v.Cv.Ease.String()
}
return strconv.Itoa(len(v.Cv.Keys)) + " keys"
case schema.KindEnum:
if name, ok := fd.EnumNameOf(v.I); ok {
return name
}
return strconv.FormatInt(v.I, 10)
case schema.KindAssetRef:
return assetState(p.d.Pipeline, v.Asset).label
case schema.KindNodeRef:
if d == nil {
return "—"
}
return nodeState(d.Scene(), v.Node, fd.Component).label
case schema.KindList:
return "[" + strconv.Itoa(len(v.List)) + "]"
}
return "…"
}
func elemOf(fd *schema.FieldDescriptor) schema.FieldDescriptor {
return schema.FieldDescriptor{
Name: fd.Name,
Kind: fd.Elem,
Struct: fd.Struct,
Enum: fd.Enum,
EnumName: fd.EnumName,
AssetKind: fd.AssetKind,
Component: fd.Component,
Flags: fd.Flags,
Min: fd.Min,
Max: fd.Max,
HasMin: fd.HasMin,
HasMax: fd.HasMax,
Hints: fd.Hints,
}
}
func (p *Panel) foldHeader(ui *core.Context, id core.ID, r mathx.Rect, text string) bool {
th := &ui.Theme
if ui.Pressed(id, r) {
p.collapsed[id] = !p.collapsed[id]
}
open := !p.collapsed[id]
widgets.Chevron(ui, mathx.Rect{X: r.X, Y: r.Y, W: th.Indent, H: r.H}, open, th.TextDim)
ui.Text(text, mathx.Vec2{X: r.X + th.Indent, Y: ui.TextY(r)}, th.TextDim)
return open
}
func (p *Panel) listValue(d *document.SceneDocument, fd *schema.FieldDescriptor, v *schema.Value, mixed bool, label string, ro bool) widgets.Edit {
ui := p.f.UI
th := &ui.Theme
row := ui.Cursor().Next(0)
id := ui.ID("list")
n := len(v.List)
bw := row.H
hdr := row
editable := !ro && !mixed
if editable {
hdr.W -= 2*bw + 4
}
text := label + " [" + strconv.Itoa(n) + "]"
if mixed {
text = label + " " + th.Mixed
}
open := p.foldHeader(ui, id, hdr, text)
var e widgets.Edit
if editable {
elem := elemOf(fd)
if widgets.ButtonRect(ui, core.IDSeq(id, 1), mathx.Rect{X: row.X + row.W - 2*bw - 2, Y: row.Y, W: bw, H: row.H}, "+") {
v.List = append(v.List, zeroValue(p.schemaOf(d), &elem))
e = all
}
if widgets.ButtonRect(ui, core.IDSeq(id, 2), mathx.Rect{X: row.X + row.W - bw, Y: row.Y, W: bw, H: row.H}, "-") && n > 0 {
v.List = v.List[:n-1]
e = all
}
}
if !open || mixed {
return e
}
cur := ui.Cursor()
cur.X += th.Indent
cur.W -= th.Indent
elem := elemOf(fd)
for i := range v.List {
ui.PushIDInt(i)
e = e.Or(p.value(d, &elem, &v.List[i], false, strconv.Itoa(i), "", ro))
ui.PopID()
}
cur.X -= th.Indent
cur.W += th.Indent
return e
}
func (p *Panel) structValue(d *document.SceneDocument, fd *schema.FieldDescriptor, v *schema.Value, mixed bool, label string, ro bool) widgets.Edit {
ui := p.f.UI
th := &ui.Theme
reg := p.schemaOf(d)
if reg == nil {
return widgets.Edit{}
}
td, ok := reg.ByID(fd.Struct)
if !ok {
return widgets.Edit{}
}
row := ui.Cursor().Next(0)
text := label
if mixed {
text += " " + th.Mixed
}
if !p.foldHeader(ui, ui.ID("struct"), row, text) || mixed {
return widgets.Edit{}
}
if v.Obj == nil || len(v.Obj.Fields) != len(td.Fields) {
*v = zeroValue(reg, fd)
}
cur := ui.Cursor()
cur.X += th.Indent
cur.W -= th.Indent
var e widgets.Edit
for i := range td.Fields {
sub := &td.Fields[i]
if !sub.Flags.Has(schema.FlagEditor) || sub.Hints.Hidden {
continue
}
ui.PushIDInt(i)
e = e.Or(p.value(d, sub, &v.Obj.Fields[i], false, p.labelOf(sub), sub.Hints.Tooltip, ro || sub.Flags.Has(schema.FlagReadOnly)))
ui.PopID()
}
cur.X -= th.Indent
cur.W += th.Indent
return e
}