168 lines
4.9 KiB
Go
168 lines
4.9 KiB
Go
package gameview
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/editor/core/play"
|
|
"aego/platform/input"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
// Scrubber is the part of the play controller the timeline bar drives.
|
|
type Scrubber interface {
|
|
Timeline() play.Timeline
|
|
State() play.State
|
|
Ticks() uint64
|
|
Seek(n int) error
|
|
Scrub(n int) error
|
|
}
|
|
|
|
type TimelineBar struct {
|
|
src Scrubber
|
|
bar mathx.Rect
|
|
dragging bool
|
|
dragTo int
|
|
scrubbed int
|
|
seekErr string
|
|
}
|
|
|
|
func NewTimelineBar(src Scrubber) *TimelineBar {
|
|
return &TimelineBar{src: src}
|
|
}
|
|
|
|
func (b *TimelineBar) Reset() {
|
|
b.bar, b.dragging = mathx.Rect{}, false
|
|
}
|
|
|
|
func (b *TimelineBar) Clear() {
|
|
b.bar = mathx.Rect{}
|
|
}
|
|
|
|
func (b *TimelineBar) Holds(m mathx.Vec2) bool {
|
|
return b.dragging || b.bar.Contains(m)
|
|
}
|
|
|
|
func megabytes(b int) string {
|
|
return strconv.FormatFloat(float64(b)/(1<<20), 'f', 1, 64) + " MB"
|
|
}
|
|
|
|
func (b *TimelineBar) idle(ui *core.Context, bar mathx.Rect, tl play.Timeline) {
|
|
th := &ui.Theme
|
|
dot := mathx.Rect{X: bar.X + 10, Y: bar.Y + bar.H/2 - 4, W: 8, H: 8}
|
|
ui.Box(dot, 4, th.Widget, th.TextDim)
|
|
msg := "Recording starts with the first frame · press Play or Step"
|
|
if tl.Why != "" {
|
|
msg = "History cleared: " + tl.Why + " · press Play or Step to record again"
|
|
}
|
|
right := "tick " + strconv.FormatUint(b.src.Ticks(), 10)
|
|
switch {
|
|
case tl.KeysOff != "":
|
|
right = "keyframes off: " + tl.KeysOff + " · " + right
|
|
case tl.Keys > 0:
|
|
right = strconv.Itoa(tl.Keys) + " key, " + megabytes(tl.KeyBytes) + " · " + right
|
|
}
|
|
rw := min(ui.TextSize(right).X+12, bar.W*0.4)
|
|
tx := dot.X + dot.W + 8
|
|
ui.PushClip(mathx.Rect{X: tx, Y: bar.Y, W: max(0, bar.X+bar.W-rw-tx-8), H: bar.H})
|
|
ui.TextIn(mathx.Rect{X: tx, Y: bar.Y, W: bar.W, H: bar.H}, msg, th.TextDim, core.AlignLeft)
|
|
ui.PopClip()
|
|
ui.TextIn(mathx.Rect{X: bar.X + bar.W - rw - 6, Y: bar.Y, W: rw, H: bar.H}, right, th.TextDisabled, core.AlignRight)
|
|
}
|
|
|
|
func (b *TimelineBar) Draw(ui *core.Context, r mathx.Rect) {
|
|
tl := b.src.Timeline()
|
|
if b.src.State() == play.Stopped && tl.Blocked == "" {
|
|
return
|
|
}
|
|
th := &ui.Theme
|
|
bar := mathx.Rect{X: r.X + 6, Y: r.Y + r.H - th.BarHeight - 6, W: max(0, r.W-12), H: th.BarHeight}
|
|
b.bar = bar
|
|
ui.PushClip(r)
|
|
defer ui.PopClip()
|
|
ui.Rect(bar, th.PanelAlt)
|
|
ui.Outline(bar, th.BarBorder)
|
|
if tl.Blocked != "" {
|
|
ui.TextIn(bar, tl.Blocked, th.TextDim, core.AlignCenter)
|
|
return
|
|
}
|
|
if tl.Frames == 0 {
|
|
b.idle(ui, bar, tl)
|
|
return
|
|
}
|
|
type part struct {
|
|
text string
|
|
col mathx.Color
|
|
}
|
|
parts := []part{{"tick " + strconv.FormatUint(tl.Tick, 10), th.Text}, {" " + strconv.Itoa(tl.Cursor) + " / " + strconv.Itoa(tl.Frames), th.TextDim}}
|
|
switch {
|
|
case b.seekErr != "":
|
|
parts = []part{{b.seekErr, th.Error}}
|
|
case tl.Why != "":
|
|
parts = append(parts, part{" · ended: " + tl.Why, th.Warn})
|
|
case tl.KeysOff != "":
|
|
parts = append(parts, part{" · keyframes off: " + tl.KeysOff, th.TextDisabled})
|
|
case tl.Keys > 0:
|
|
parts = append(parts, part{" · " + strconv.Itoa(tl.Keys) + " keys, " + megabytes(tl.KeyBytes), th.TextDisabled})
|
|
}
|
|
full := float32(0)
|
|
for _, pt := range parts {
|
|
full += ui.TextSize(pt.text).X
|
|
}
|
|
lw := min(full+16, bar.W*0.45)
|
|
track := mathx.Rect{X: bar.X + 10, Y: bar.Y + bar.H/2 - 3, W: max(1, bar.W-lw-24), H: 6}
|
|
total := max(tl.Frames, 1)
|
|
xAt := func(n int) float32 { return track.X + track.W*float32(n)/float32(total) }
|
|
frameAt := func(x float32) int {
|
|
return int(mathx.Clamp((x-track.X)/track.W, 0, 1)*float32(total) + 0.5)
|
|
}
|
|
ui.Rect(track, th.Widget)
|
|
lo := xAt(tl.Floor)
|
|
ui.Rect(mathx.Rect{X: lo, Y: track.Y, W: max(0, xAt(tl.Usable)-lo), H: track.H}, th.AccentDim)
|
|
id := ui.ID("timeline")
|
|
hit := mathx.Rect{X: track.X - 4, Y: bar.Y, W: track.W + 8, H: bar.H}
|
|
if ui.Pressed(id, hit) {
|
|
b.dragging, b.seekErr, b.scrubbed = true, "", -1
|
|
}
|
|
cur := tl.Cursor
|
|
if tl.Beyond {
|
|
cur = tl.Frames
|
|
}
|
|
if b.dragging {
|
|
b.dragTo = min(max(frameAt(ui.Mouse().X), tl.Floor), tl.Usable)
|
|
cur = b.dragTo
|
|
if b.dragTo != b.scrubbed && b.seekErr == "" {
|
|
b.scrubbed = b.dragTo
|
|
if err := b.src.Scrub(b.dragTo); err != nil {
|
|
b.seekErr = err.Error()
|
|
}
|
|
}
|
|
if !ui.MouseDown(input.MouseLeft) {
|
|
b.dragging = false
|
|
if err := b.src.Seek(b.dragTo); err != nil {
|
|
b.seekErr = err.Error()
|
|
}
|
|
}
|
|
}
|
|
hx := xAt(cur)
|
|
ui.Rect(mathx.Rect{X: hx - 2, Y: bar.Y + 3, W: 4, H: bar.H - 6}, th.Accent)
|
|
lr := mathx.Rect{X: track.X + track.W + 10, Y: bar.Y, W: lw - 4, H: bar.H}
|
|
ui.PushClip(lr)
|
|
x := lr.X
|
|
for _, pt := range parts {
|
|
ui.Text(pt.text, mathx.Vec2{X: x, Y: ui.TextY(lr)}, pt.col)
|
|
x += ui.TextSize(pt.text).X
|
|
}
|
|
ui.PopClip()
|
|
tip := "Drag to rewind the game; Ctrl+Left and Ctrl+Right step one frame"
|
|
if tl.Keys > 0 {
|
|
tip += "\n" + strconv.Itoa(tl.Keys) + " keyframes, " + megabytes(tl.KeyBytes) + "; the last took " + megabytes(tl.LastKey) +
|
|
" and " + strconv.FormatFloat(float64(tl.KeyTime.Microseconds())/1000, 'f', 2, 64) + " ms"
|
|
}
|
|
if tl.Why != "" {
|
|
tip += "\nHistory ends: " + tl.Why
|
|
}
|
|
widgets.Tooltip(ui, id, tip)
|
|
}
|