// SPDX-License-Identifier: Unlicense OR MIT package rendertest import ( "image" "image/color" "math" "testing" "golang.org/x/image/colornames" "gioui.org/f32" "gioui.org/internal/f32color" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" ) func TestTransformMacro(t *testing.T) { // testcase resulting from original bug when rendering layout.Stacked // Build clip-path. c := constSqPath() run(t, func(o *op.Ops) { // render the first Stacked item m1 := op.Record(o) dr := image.Rect(0, 0, 128, 50) paint.FillShape(o, black, clip.Rect(dr).Op()) c1 := m1.Stop() // Render the second stacked item m2 := op.Record(o) paint.ColorOp{Color: red}.Add(o) // Simulate a draw text call t := op.Offset(image.Pt(0, 10)).Push(o) // Apply the clip-path. cl := c.Push(o) paint.PaintOp{}.Add(o) cl.Pop() t.Pop() c2 := m2.Stop() // Call each of them in a transform t = op.Offset(image.Pt(0, 0)).Push(o) c1.Add(o) t.Pop() t = op.Offset(image.Pt(0, 0)).Push(o) c2.Add(o) t.Pop() }, func(r result) { r.expect(5, 15, colornames.Red) r.expect(15, 15, colornames.Black) r.expect(11, 51, transparent) }) } func TestRepeatedPaintsZ(t *testing.T) { run(t, func(o *op.Ops) { // Draw a rectangle paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) builder := clip.Path{} builder.Begin(o) builder.Move(f32.Pt(0, 0)) builder.Line(f32.Pt(10, 0)) builder.Line(f32.Pt(0, 10)) builder.Line(f32.Pt(-10, 0)) builder.Line(f32.Pt(0, -10)) p := builder.End() defer clip.Outline{ Path: p, }.Op().Push(o).Pop() paint.Fill(o, red) }, func(r result) { r.expect(5, 5, colornames.Red) r.expect(11, 15, colornames.Black) r.expect(11, 51, transparent) }) } func TestNoClipFromPaint(t *testing.T) { // ensure that a paint operation does not pollute the state // by leaving any clip paths in place. run(t, func(o *op.Ops) { a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4) defer op.Affine(a).Push(o).Pop() paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4) defer op.Affine(a).Push(o).Pop() paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) }, func(r result) { r.expect(1, 1, colornames.Black) r.expect(20, 20, colornames.Black) r.expect(49, 49, colornames.Black) r.expect(51, 51, transparent) }) } func TestDeferredPaint(t *testing.T) { run(t, func(o *op.Ops) { cl := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o) paint.ColorOp{Color: color.NRGBA{A: 0x60, G: 0xff}}.Add(o) paint.PaintOp{}.Add(o) cl.Pop() t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Push(o) m := op.Record(o) cl2 := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o) paint.ColorOp{Color: color.NRGBA{A: 0x60, R: 0xff, G: 0xff}}.Add(o) paint.PaintOp{}.Add(o) cl2.Pop() paintMacro := m.Stop() op.Defer(o, paintMacro) t.Pop() defer op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Push(o).Pop() defer clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o).Pop() paint.ColorOp{Color: color.NRGBA{A: 0x60, B: 0xff}}.Add(o) paint.PaintOp{}.Add(o) }, func(r result) { }) } func constSqPath() clip.Op { innerOps := new(op.Ops) builder := clip.Path{} builder.Begin(innerOps) builder.Move(f32.Pt(0, 0)) builder.Line(f32.Pt(10, 0)) builder.Line(f32.Pt(0, 10)) builder.Line(f32.Pt(-10, 0)) builder.Line(f32.Pt(0, -10)) p := builder.End() return clip.Outline{Path: p}.Op() } func constSqCirc() clip.Op { innerOps := new(op.Ops) return clip.RRect{Rect: f32.Rect(0, 0, 40, 40), NW: 20, NE: 20, SW: 20, SE: 20}.Op(innerOps) } func drawChild(ops *op.Ops, text clip.Op) op.CallOp { r1 := op.Record(ops) cl := text.Push(ops) paint.PaintOp{}.Add(ops) cl.Pop() return r1.Stop() } func TestReuseStencil(t *testing.T) { txt := constSqPath() run(t, func(ops *op.Ops) { c1 := drawChild(ops, txt) c2 := drawChild(ops, txt) // lay out the children c1.Add(ops) defer op.Offset(image.Pt(0, 50)).Push(ops).Pop() c2.Add(ops) }, func(r result) { r.expect(5, 5, colornames.Black) r.expect(5, 55, colornames.Black) }) } func TestBuildOffscreen(t *testing.T) { // Check that something we in one frame build outside the screen // still is rendered correctly if moved into the screen in a later // frame. txt := constSqCirc() draw := func(off int, o *op.Ops) { defer op.Offset(image.Pt(0, off)).Push(o).Pop() defer txt.Push(o).Pop() paint.PaintOp{}.Add(o) } multiRun(t, frame( func(ops *op.Ops) { draw(-100, ops) }, func(r result) { r.expect(5, 5, transparent) r.expect(20, 20, transparent) }), frame( func(ops *op.Ops) { draw(0, ops) }, func(r result) { r.expect(2, 2, transparent) r.expect(20, 20, colornames.Black) r.expect(38, 38, transparent) })) } func TestNegativeOverlaps(t *testing.T) { run(t, func(ops *op.Ops) { defer clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Push(ops).Pop() clip.Rect(image.Rect(0, 120, 100, 122)).Push(ops).Pop() paint.PaintOp{}.Add(ops) }, func(r result) { r.expect(60, 60, transparent) r.expect(60, 110, transparent) r.expect(60, 120, transparent) r.expect(60, 122, transparent) }) } func TestDepthOverlap(t *testing.T) { run(t, func(ops *op.Ops) { paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) }, func(r result) { r.expect(96, 32, colornames.Red) r.expect(32, 96, colornames.Green) r.expect(32, 32, colornames.Green) }) } type Gradient struct { From, To color.NRGBA } var gradients = []Gradient{ {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, } func TestLinearGradient(t *testing.T) { t.Skip("linear gradients don't support transformations") const gradienth = 8 // 0.5 offset from ends to ensure that the center of the pixel // aligns with gradient from and to colors. pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) samples := []int{0, 12, 32, 64, 96, 115, 127} run(t, func(ops *op.Ops) { gr := f32.Rect(0, 0, 128, gradienth) for _, g := range gradients { paint.LinearGradientOp{ Stop1: f32.Pt(gr.Min.X, gr.Min.Y), Color1: g.From, Stop2: f32.Pt(gr.Max.X, gr.Min.Y), Color2: g.To, }.Add(ops) cl := clip.RRect{Rect: gr}.Push(ops) t1 := op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Push(ops) t2 := scale(pixelAligned.Dx()/128, 1).Push(ops) paint.PaintOp{}.Add(ops) t2.Pop() t1.Pop() cl.Pop() gr = gr.Add(f32.Pt(0, gradienth)) } }, func(r result) { gr := pixelAligned for _, g := range gradients { from := f32color.LinearFromSRGB(g.From) to := f32color.LinearFromSRGB(g.To) for _, p := range samples { exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB())) } gr = gr.Add(f32.Pt(0, gradienth)) } }) } func TestLinearGradientAngled(t *testing.T) { run(t, func(ops *op.Ops) { paint.LinearGradientOp{ Stop1: f32.Pt(64, 64), Color1: black, Stop2: f32.Pt(0, 0), Color2: red, }.Add(ops) cl := clip.Rect(image.Rect(0, 0, 64, 64)).Push(ops) paint.PaintOp{}.Add(ops) cl.Pop() paint.LinearGradientOp{ Stop1: f32.Pt(64, 64), Color1: white, Stop2: f32.Pt(128, 0), Color2: green, }.Add(ops) cl = clip.Rect(image.Rect(64, 0, 128, 64)).Push(ops) paint.PaintOp{}.Add(ops) cl.Pop() paint.LinearGradientOp{ Stop1: f32.Pt(64, 64), Color1: black, Stop2: f32.Pt(128, 128), Color2: blue, }.Add(ops) cl = clip.Rect(image.Rect(64, 64, 128, 128)).Push(ops) paint.PaintOp{}.Add(ops) cl.Pop() paint.LinearGradientOp{ Stop1: f32.Pt(64, 64), Color1: white, Stop2: f32.Pt(0, 128), Color2: magenta, }.Add(ops) cl = clip.Rect(image.Rect(0, 64, 64, 128)).Push(ops) paint.PaintOp{}.Add(ops) cl.Pop() }, func(r result) {}) } func TestZeroImage(t *testing.T) { ops := new(op.Ops) w := newWindow(t, 10, 10) paint.ImageOp{}.Add(ops) paint.PaintOp{}.Add(ops) if err := w.Frame(ops); err != nil { t.Error(err) } } func TestImageRGBA(t *testing.T) { run(t, func(o *op.Ops) { w := newWindow(t, 10, 10) im := image.NewRGBA(image.Rect(0, 0, 5, 5)) im.Set(3, 3, colornames.Red) im.Set(4, 3, colornames.Red) im.Set(3, 4, colornames.Red) im.Set(4, 4, colornames.Red) im = im.SubImage(image.Rect(2, 2, 5, 5)).(*image.RGBA) paint.NewImageOp(im).Add(o) paint.PaintOp{}.Add(o) if err := w.Frame(o); err != nil { t.Error(err) } }, func(r result) { r.expect(1, 1, colornames.Red) r.expect(2, 1, colornames.Red) r.expect(1, 2, colornames.Red) r.expect(2, 2, colornames.Red) }) } func TestGapsInPath(t *testing.T) { ops := new(op.Ops) var p clip.Path p.Begin(ops) // Unclosed square 1 p.MoveTo(f32.Point{X: 10}) p.LineTo(f32.Point{X: 40}) p.LineTo(f32.Point{X: 40, Y: 30}) p.LineTo(f32.Point{X: 10, Y: 30}) // Unclosed square 2 p.MoveTo(f32.Point{X: 50}) p.LineTo(f32.Point{X: 80}) p.LineTo(f32.Point{X: 80, Y: 30}) p.LineTo(f32.Point{X: 50, Y: 30}) spec := p.End() t.Run("Stroke", func(t *testing.T) { run(t, func(ops *op.Ops) { stack := clip.Stroke{ Path: spec, Width: 2, }.Op().Push(ops) paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops) paint.PaintOp{}.Add(ops) stack.Pop() }, func(r result) { r.expect(10, 20, color.RGBA{}) r.expect(50, 20, color.RGBA{}) }, ) }) t.Run("Outline", func(t *testing.T) { run(t, func(ops *op.Ops) { stack := clip.Outline{Path: spec}.Op().Push(ops) paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops) paint.PaintOp{}.Add(ops) stack.Pop() }, func(r result) { r.expect(10, 20, colornames.Red) r.expect(20, 20, colornames.Red) r.expect(50, 20, colornames.Red) r.expect(60, 20, colornames.Red) }, ) }) } // lerp calculates linear interpolation with color b and p. func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { return f32color.RGBA{ R: a.R*(1-p) + b.R*p, G: a.G*(1-p) + b.G*p, B: a.B*(1-p) + b.B*p, A: a.A*(1-p) + b.A*p, } }