Files
aego/app/edit/art_duration_gl_test.go

242 lines
7.7 KiB
Go

//go:build aego_gltest && linux && cgo && !aego_nonative
package edit
import (
"bytes"
"context"
"image"
"os"
"path/filepath"
"runtime"
"testing"
"aego/art"
"aego/core/log"
"aego/editor/core/artedit"
"aego/editor/core/workspace"
"aego/platform/input"
"aego/platform/sdl"
)
// Opens a test project through Go, then drives the native timeline through
// synthetic SDL pointer/text/key events; this is not physical OS/GPU input.
func TestArtTimelineDurationNative(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("XDG_DATA_HOME", t.TempDir())
p := newProject(t)
drawing, err := art.New(16, 16, art.RGBA, art.DefaultLimits())
if err != nil {
t.Fatal(err)
}
initial, err := drawing.Encode()
drawing.Close()
if err != nil {
t.Fatal(err)
}
filename := filepath.Join(p.Root, "assets", "duration.aeart")
if err := os.WriteFile(filename, initial, 0644); err != nil {
t.Fatal(err)
}
s, err := Open(p, Config{Hidden: true, NoWatch: true, Width: 1600, Height: 900, Logger: log.Nop()})
if err != nil {
t.Fatal(err)
}
defer func() {
if s != nil {
s.Close()
}
}()
if !s.Step() {
t.Fatal("editor stopped during startup")
}
ed, ws := s.App.Context(), s.App.Workspace()
if !ws.Switch(workspace.Art) {
t.Fatal("Art workspace is unavailable")
}
id, ok := ed.Pipeline.Lookup("assets/duration.aeart")
if !ok {
t.Fatal("drawing was not imported")
}
doc, err := ed.OpenAsset(id)
if err != nil {
t.Fatal(err)
}
model, ok := doc.Binding.Model.(*artedit.Document)
if !ok {
t.Fatal("drawing model is unavailable")
}
for range 8 {
if !s.Step() {
t.Fatal("editor stopped while laying out Art")
}
}
timeline, visible := ws.Tree.PanelRect("art.timeline")
if !visible {
t.Fatal("Art timeline is not visible")
}
pad, row, gap := ed.UI.Theme.Padding, ed.UI.Theme.RowHeight, ed.UI.Theme.Spacing
fieldX := timeline.X + pad + (timeline.W-2*pad)/6
fieldY := timeline.Y + pad + 3*(row+gap) + row/2
addX := timeline.X + pad + (timeline.W-2*pad)/8
addY := timeline.Y + pad + 2*(row+gap) + row/2
click := func(x, y float32) {
t.Helper()
for i := range 3 {
if !sdl.TestPointerEvent(x, y, i) || !s.Step() {
t.Fatal("SDL pointer input failed")
}
}
}
typeText := func(text string) {
t.Helper()
if !sdl.TestTextInput(text) || !s.Step() {
t.Fatal("SDL text input failed")
}
}
key := func(k input.Scancode, ctrl, shift bool) {
t.Helper()
if !sdl.TestEditKey(k, true, ctrl, shift) || !s.Step() || !sdl.TestEditKey(k, false, false, false) || !s.Step() {
t.Fatal("SDL key input failed")
}
}
capture := func(r image.Rectangle) []byte {
t.Helper()
var img *image.RGBA
var captureErr error
s.Engine.CaptureFrame(func(p *image.RGBA, err error) { img, captureErr = p, err })
if !s.Step() || captureErr != nil || img == nil {
t.Fatalf("timeline capture failed: %v", captureErr)
}
r = r.Intersect(img.Bounds())
if r.Empty() {
t.Fatal("timeline capture is empty")
}
var pixels []byte
for y := r.Min.Y; y < r.Max.Y; y++ {
start := img.PixOffset(r.Min.X, y)
pixels = append(pixels, img.Pix[start:start+4*r.Dx()]...)
}
return pixels
}
// Compare the stable inside of the duration field, not its hover border.
fieldW := (timeline.W - 2*pad - 8) / 3
fieldRect := image.Rect(int(timeline.X+pad+5), int(fieldY-row/2+3), int(timeline.X+pad+min(fieldW-5, 80)), int(fieldY+row/2-3))
want100 := capture(fieldRect)
click(fieldX, fieldY)
if !ed.UI.WantsKeyboard() {
t.Fatal("duration field did not take keyboard focus")
}
typeText("250")
key(input.KeyEnter, false, false)
if frames := model.Session.Info().Document.Frames; len(frames) != 1 || frames[0].DurationMS != 250 {
t.Fatalf("typing duration did not update the first frame: %+v", frames)
}
// Blur an uncommitted edit by clicking Add Frame. It belongs to the first
// frame even though the new frame is selected on the same mouse release.
click(fieldX, fieldY)
typeText("275")
click(addX, addY)
frames := model.Session.Info().Document.Frames
if len(frames) != 2 || frames[0].DurationMS != 275 || frames[1].DurationMS != 100 {
t.Fatalf("SDL Add Frame moved the pending duration to the wrong frame: %+v", frames)
}
if got := capture(fieldRect); !bytes.Equal(got, want100) {
t.Fatal("selected new frame's displayed duration does not match the original 100 ms field")
}
playY := timeline.Y + pad + row/2
playW := (timeline.W - 2*pad - 12) / 4
playRect := image.Rect(int(timeline.X+pad+4), int(playY-row/2+4), int(timeline.X+pad+min(playW-4, 70)), int(playY+row/2-4))
moveAway := func() {
t.Helper()
if !sdl.TestPointerEvent(10, 10, 0) || !s.Step() {
t.Fatal("SDL pointer move failed")
}
}
moveAway()
paused := capture(playRect)
click(addX, playY)
moveAway()
playing := capture(playRect)
if bytes.Equal(paused, playing) {
t.Fatal("Play did not change its native toggle")
}
click(addX, playY)
moveAway()
if got := capture(playRect); !bytes.Equal(got, paused) {
t.Fatal("Pause did not restore its native toggle")
}
// Focus the drawing before routing the ordinary Undo/Redo and Save shortcuts.
ws.Focus("render2d.art/" + id.String())
key(input.KeyZ, true, false)
if frames = model.Session.Info().Document.Frames; len(frames) != 1 || frames[0].DurationMS != 275 {
t.Fatalf("SDL Undo did not remove the added frame: %+v", frames)
}
key(input.KeyZ, true, true)
if frames = model.Session.Info().Document.Frames; len(frames) != 2 || frames[0].DurationMS != 275 || frames[1].DurationMS != 100 {
t.Fatalf("SDL Redo did not restore both durations: %+v", frames)
}
// Seed a pixel through the fixture API, then exercise only real SDL
// shortcuts for Copy/Cut/Paste. Each mutation must reach one history stack.
_, err = model.Execute(context.Background(), art.Operation{Kind: "stroke", Points: []image.Point{{X: 4, Y: 4}}, Brush: art.Brush{Size: 1, Paint: art.Paint{Color: art.Color{R: 255, A: 255}}}})
if err != nil {
t.Fatal(err)
}
cursor := model.History.Cursor()
key(input.KeyC, true, false)
if model.History.Cursor() != cursor {
t.Fatal("SDL Copy changed drawing history")
}
key(input.KeyX, true, false)
cut, _, err := model.Capture(model.Session.Frame, art.Albedo)
if err != nil || cut == nil || model.History.Cursor() != cursor+1 || cut.NRGBAAt(4, 4).A != 0 {
t.Fatalf("SDL Cut did not create one art edit: %v", err)
}
key(input.KeyV, true, false)
pasted, _, err := model.Capture(model.Session.Frame, art.Albedo)
if err != nil || pasted == nil || model.History.Cursor() != cursor+2 || pasted.NRGBAAt(4, 4).A != 255 {
t.Fatalf("SDL Paste did not create one art edit: %v", err)
}
key(input.KeyS, true, false)
if doc.Dirty() {
t.Fatal("SDL Save left the drawing dirty")
}
saved, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
reopened, err := art.Decode(saved, art.DefaultLimits())
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
frames = reopened.Info().Frames
if len(frames) != 2 || frames[0].DurationMS != 275 || frames[1].DurationMS != 100 {
t.Fatalf("saved drawing lost its frame durations: %+v", frames)
}
s.Close()
s = nil
again, err := Open(p, Config{Hidden: true, NoWatch: true, Width: 1600, Height: 900, Logger: log.Nop()})
if err != nil {
t.Fatal(err)
}
defer again.Close()
if !again.Step() {
t.Fatal("editor stopped after reopening the project")
}
reopenedDoc, err := again.App.Context().OpenAsset(id)
if err != nil {
t.Fatal(err)
}
reopenedModel, ok := reopenedDoc.Binding.Model.(*artedit.Document)
if !ok {
t.Fatal("reopened drawing did not bind to the native editor")
}
frames = reopenedModel.Session.Info().Document.Frames
if len(frames) != 2 || frames[0].DurationMS != 275 || frames[1].DurationMS != 100 {
t.Fatalf("reopened editor lost its frame durations: %+v", frames)
}
}