~eliasnaur/gio-patches

[PATCH gio v3 2/2] gpu,op/clip: split xyz-ops

Details
Message ID
<6Fr99cb0tTCWUJB0EqC7l6OWEUqD1smJ3iWeDse2G0@cp4-web-036.plabs.ch>
DKIM signature
missing
Download raw message
Patch: +522 -286
Signed-off-by: Sebastien Binet <s@sbinet.org>
---
 font/opentype/opentype.go                     |   5 +-
 gpu/dash.go                                   |  64 ++--
 gpu/gpu.go                                    | 159 ++++++---
 gpu/stroke.go                                 |  18 +-
 internal/opconst/ops.go                       |  14 +-
 internal/ops/reader.go                        |   6 -
 internal/rendertest/clip_test.go              | 327 +++++++++++-------
 .../refs/TestStrokedPathFlatMiter.png         | Bin 2309 -> 2258 bytes
 .../refs/TestStrokedPathFlatMiterInf.png      | Bin 2304 -> 2262 bytes
 internal/rendertest/render_test.go            |  10 +-
 op/clip/clip.go                               |  75 ++--
 op/clip/shapes.go                             |  15 +-
 op/clip/stroke.go                             | 107 +++++-
 widget/material/loader.go                     |   8 +-
 14 files changed, 522 insertions(+), 286 deletions(-)

diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go
index 3e89cef..de96d2b 100644
--- a/font/opentype/opentype.go
+++ b/font/opentype/opentype.go
@@ -323,7 +323,10 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text.
		x += str.Advances[rune]
		rune++
	}
	builder.Outline().Add(ops)
	builder.Close()
	builder.Op().Add(ops)
	clip.OutlineOp{}.Add(ops)
	clip.Op{}.Add(ops)
	return m.Stop()
}

diff --git a/gpu/dash.go b/gpu/dash.go
index 1460228..323298f 100644
--- a/gpu/dash.go
+++ b/gpu/dash.go
@@ -12,29 +12,27 @@ import (

	"gioui.org/f32"
	"gioui.org/internal/ops"
	"gioui.org/op/clip"
)

func isSolidLine(sty clip.DashStyle) bool {
	return sty.Offset == 0 && len(sty.Dashes) == 0
func isSolidLine(sty dashOp) bool {
	return sty.phase == 0 && len(sty.dashes) == 0
}

func (qs strokeQuads) dash(sty clip.DashStyle) strokeQuads {
func (qs strokeQuads) dash(sty dashOp) strokeQuads {
	sty = dashCanonical(sty)

	switch {
	case len(sty.Dashes) == 0:
	case len(sty.dashes) == 0:
		return qs
	case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
		var o strokeQuads
		return o
	case len(sty.dashes) == 1 && sty.dashes[0] == 0.0:
		return strokeQuads{}
	}

	if len(sty.Dashes)%2 == 1 {
	if len(sty.dashes)%2 == 1 {
		// If the dash pattern is of uneven length, dash and space lengths
		// alternate. The following duplicates the pattern so that uneven
		// indices are always spaces.
		sty.Dashes = append(sty.Dashes, sty.Dashes...)
		sty.dashes = append(sty.dashes, sty.dashes...)
	}

	var (
@@ -51,13 +49,13 @@ func (qs strokeQuads) dash(sty clip.DashStyle) strokeQuads {
			t      []float64
			length = ps.len()
		)
		for pos+sty.Dashes[i] < length {
			pos += sty.Dashes[i]
		for pos+sty.dashes[i] < length {
			pos += sty.dashes[i]
			if 0.0 < pos {
				t = append(t, float64(pos))
			}
			i++
			if i == len(sty.Dashes) {
			if i == len(sty.dashes) {
				i = 0
			}
		}
@@ -88,13 +86,13 @@ func (qs strokeQuads) dash(sty clip.DashStyle) strokeQuads {
	return out
}

func dashCanonical(sty clip.DashStyle) clip.DashStyle {
func dashCanonical(sty dashOp) dashOp {
	var (
		o  = sty
		ds = o.Dashes
		ds = o.dashes
	)

	if len(sty.Dashes) == 0 {
	if len(sty.dashes) == 0 {
		return sty
	}

@@ -110,12 +108,12 @@ func dashCanonical(sty clip.DashStyle) clip.DashStyle {
	// Remove first zero, collapse with second and last.
	if f32Eq(ds[0], 0.0) {
		if len(ds) < 3 {
			return clip.DashStyle{
				Offset: 0.0,
				Dashes: []float32{0.0},
			return dashOp{
				phase: 0.0,
				dashes: []float32{0.0},
			}
		}
		o.Offset -= ds[1]
		o.phase -= ds[1]
		ds[len(ds)-1] += ds[1]
		ds = ds[2:]
	}
@@ -123,9 +121,9 @@ func dashCanonical(sty clip.DashStyle) clip.DashStyle {
	// Remove last zero, collapse with fist and second to last.
	if f32Eq(ds[len(ds)-1], 0.0) {
		if len(ds) < 3 {
			return clip.DashStyle{}
			return dashOp{}
		}
		o.Offset += ds[len(ds)-2]
		o.phase += ds[len(ds)-2]
		ds[0] += ds[len(ds)-2]
		ds = ds[:len(ds)-2]
	}
@@ -133,9 +131,9 @@ func dashCanonical(sty clip.DashStyle) clip.DashStyle {
	// If there are zeros or negatives, don't draw dashes.
	for i := 0; i < len(ds); i++ {
		if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
			return clip.DashStyle{
				Offset: 0.0,
				Dashes: []float32{0.0},
			return dashOp{
				phase: 0.0,
				dashes: []float32{0.0},
			}
		}
	}
@@ -154,23 +152,23 @@ loop:
	return o
}

func dashStart(sty clip.DashStyle) (int, float32) {
func dashStart(sty dashOp) (int, float32) {
	i0 := 0 // i0 is the index into dashes.
	for sty.Dashes[i0] <= sty.Offset {
		sty.Offset -= sty.Dashes[i0]
	for sty.dashes[i0] <= sty.phase {
		sty.phase -= sty.dashes[i0]
		i0++
		if i0 == len(sty.Dashes) {
		if i0 == len(sty.dashes) {
			i0 = 0
		}
	}
	// pos0 may be negative if the offset lands halfway into dash.
	pos0 := -sty.Offset
	if sty.Offset < 0.0 {
	pos0 := -sty.phase
	if sty.phase < 0.0 {
		var sum float32
		for _, d := range sty.Dashes {
		for _, d := range sty.dashes {
			sum += d
		}
		pos0 = -(sum + sty.Offset) // handle negative offsets
		pos0 = -(sum + sty.phase) // handle negative offsets
	}
	return i0, pos0
}
diff --git a/gpu/gpu.go b/gpu/gpu.go
index 5f46862..08484d6 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -109,6 +109,52 @@ type imageOp struct {
	place    placement
}

type dashOp struct {
	phase  float32
	dashes []float32
}

func decodeDashOp(data []byte) dashOp {
	_ = data[5]
	if opconst.OpType(data[0]) != opconst.TypeDash {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return dashOp{
		phase:  math.Float32frombits(bo.Uint32(data[1:])),
		dashes: make([]float32, data[5]),
	}
}

func decodeStrokeOp(data []byte) clip.StrokeOp {
	_ = data[10]
	if opconst.OpType(data[0]) != opconst.TypeStroke {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return clip.StrokeOp{
		Width: math.Float32frombits(bo.Uint32(data[1:])),
		Miter: math.Float32frombits(bo.Uint32(data[5:])),
		Cap:   clip.StrokeCap(data[9]),
		Join:  clip.StrokeJoin(data[10]),
	}
}

type quadsOp struct {
	quads uint32
	key   ops.Key
	aux   []byte
}

func decodeQuadsOp(data []byte) uint32 {
	_ = data[:1+4]
	if opconst.OpType(data[0]) != opconst.TypePath {
		panic("invalid op")
	}
	bo := binary.LittleEndian
	return bo.Uint32(data[1:])
}

type material struct {
	material materialType
	opaque   bool
@@ -126,8 +172,6 @@ type material struct {
type clipOp struct {
	// TODO: Use image.Rectangle?
	bounds f32.Rectangle
	width  float32
	style  clip.StrokeStyle
}

// imageOpData is the shadow of paint.ImageOp.
@@ -160,18 +204,6 @@ func (op *clipOp) decode(data []byte) {
	}
	*op = clipOp{
		bounds: layout.FRect(r),
		width:  math.Float32frombits(bo.Uint32(data[17:])),
		style: clip.StrokeStyle{
			Cap:   clip.StrokeCap(data[21]),
			Join:  clip.StrokeJoin(data[22]),
			Miter: math.Float32frombits(bo.Uint32(data[23:])),
		},
	}
	op.style.Line.Offset = math.Float32frombits(bo.Uint32(data[27:]))
	op.style.Line.Dashes = make([]float32, data[31])
	dashes := data[32:]
	for i := range op.style.Line.Dashes {
		op.style.Line.Dashes[i] = math.Float32frombits(bo.Uint32(dashes[i*4:]))
	}
}

@@ -792,8 +824,12 @@ func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
}

func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
	var aux []byte
	var auxKey ops.Key
	var (
		quads   quadsOp
		outline bool
		stroke  clip.StrokeOp
		dashes  dashOp
	)
loop:
	for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
@@ -802,38 +838,72 @@ loop:
		case opconst.TypeTransform:
			dop := ops.DecodeTransform(encOp.Data)
			state.t = state.t.Mul(dop)
		case opconst.TypeAux:
			aux = encOp.Data[opconst.TypeAuxLen:]
			auxKey = encOp.Key

		case opconst.TypeDash:
			dashes = decodeDashOp(encOp.Data)
			if len(dashes.dashes) > 0 {
				encOp, ok = r.Decode()
				if !ok {
					break loop
				}
				data := encOp.Data[1:]
				bo := binary.LittleEndian
				for i := range dashes.dashes {
					dashes.dashes[i] = math.Float32frombits(bo.Uint32(
						data[i*4:],
					))
				}
			}

		case opconst.TypeOutline:
			outline = true

		case opconst.TypeStroke:
			stroke = decodeStrokeOp(encOp.Data)

		case opconst.TypePath:
			quads.quads = decodeQuadsOp(encOp.Data)
			if quads.quads > 0 {
				encOp, ok = r.Decode()
				if !ok {
					break loop
				}
				quads.aux = encOp.Data[opconst.TypeAuxLen:]
				quads.key = encOp.Key
			}

		case opconst.TypeClip:
			var op clipOp
			op.decode(encOp.Data)
			bounds := op.bounds
			trans, off := splitTransform(state.t)
			if len(aux) > 0 {
			if len(quads.aux) > 0 {
				// There is a clipping path, build the gpu data and update the
				// cache key such that it will be equal only if the transform is the
				// same also. Use cached data if we have it.
				auxKey = auxKey.SetTransform(trans)
				if v, ok := d.pathCache.get(auxKey); ok {
				quads.key = quads.key.SetTransform(trans)
				if v, ok := d.pathCache.get(quads.key); ok {
					// Since the GPU data exists in the cache aux will not be used.
					// Why is this not used for the offset shapes?
					op.bounds = v.bounds
				} else {
					aux, op.bounds = d.buildVerts(aux, trans, op.width, op.style)
					quads.aux, op.bounds = d.buildVerts(quads.aux, trans, outline, stroke, dashes)
					// add it to the cache, without GPU data, so the transform can be
					// reused.
					d.pathCache.put(auxKey, opCacheValue{bounds: op.bounds})
					d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
				}
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
				auxKey = encOp.Key
				auxKey.SetTransform(trans)
				quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
				quads.key = encOp.Key
				quads.key.SetTransform(trans)
			}
			state.clip = state.clip.Intersect(op.bounds.Add(off))
			d.addClipPath(&state, aux, auxKey, op.bounds, off)
			aux = nil
			auxKey = ops.Key{}
			d.addClipPath(&state, quads.aux, quads.key, op.bounds, off)
			quads = quadsOp{}
			outline = false
			stroke = clip.StrokeOp{}
			dashes = dashOp{}

		case opconst.TypeColor:
			state.matType = materialColor
			state.color = decodeColorOp(encOp.Data)
@@ -1219,7 +1289,7 @@ func (d *drawOps) writeVertCache(n int) []byte {
}

// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeOp, dashes dashOp) (verts []byte, bounds f32.Rectangle) {
	inf := float32(math.Inf(+1))
	d.qs.bounds = f32.Rectangle{
		Min: f32.Point{X: inf, Y: inf},
@@ -1230,18 +1300,7 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli
	startLength := len(d.vertCache)

	switch {
	default:
		// Outline path.
		for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
			d.qs.contour = bo.Uint32(aux)
			quad := ops.DecodeQuad(aux[4:])
			quad = quad.Transform(tr)

			d.qs.splitAndEncode(quad)

			aux = aux[ops.QuadSize+4:]
		}
	case width > 0:
	case stroke.Width > 0:
		// Stroke path.
		quads := make(strokeQuads, 0, 2*len(aux)/(ops.QuadSize+4))
		for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
@@ -1252,13 +1311,25 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli
			quads = append(quads, quad)
			aux = aux[ops.QuadSize+4:]
		}
		quads = quads.stroke(width, sty)
		quads = quads.stroke(stroke, dashes)
		for _, quad := range quads {
			d.qs.contour = quad.contour
			quad.quad = quad.quad.Transform(tr)

			d.qs.splitAndEncode(quad.quad)
		}

	case outline:
		// Outline path.
		for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
			d.qs.contour = bo.Uint32(aux)
			quad := ops.DecodeQuad(aux[4:])
			quad = quad.Transform(tr)

			d.qs.splitAndEncode(quad)

			aux = aux[ops.QuadSize+4:]
		}
	}

	fillMaxY(d.vertCache[startLength:])
diff --git a/gpu/stroke.go b/gpu/stroke.go
index 7db08ae..6f3fa48 100644
--- a/gpu/stroke.go
+++ b/gpu/stroke.go
@@ -118,18 +118,18 @@ func (qs strokeQuads) split() []strokeQuads {
	return o
}

func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads {
	if !isSolidLine(sty.Line) {
		qs = qs.dash(sty.Line)
func (qs strokeQuads) stroke(stroke clip.StrokeOp, dashes dashOp) strokeQuads {
	if !isSolidLine(dashes) {
		qs = qs.dash(dashes)
	}

	var (
		o  strokeQuads
		hw = 0.5 * width
		hw = 0.5 * stroke.Width
	)

	for _, ps := range qs.split() {
		rhs, lhs := ps.offset(hw, sty)
		rhs, lhs := ps.offset(hw, stroke)
		switch lhs {
		case nil:
			o = o.append(rhs)
@@ -155,7 +155,7 @@ func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads {
// offset returns the right-hand and left-hand sides of the path, offset by
// the half-width hw.
// The stroke style sty handles how segments are joined and ends are capped.
func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQuads) {
func (qs strokeQuads) offset(hw float32, sty clip.StrokeOp) (rhs, lhs strokeQuads) {
	var (
		states []strokeState
		beg    = qs[0].quad.From
@@ -466,7 +466,7 @@ func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32

// strokePathJoin joins the two paths rhs and lhs, according to the provided
// stroke style sty.
func strokePathJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
func strokePathJoin(sty clip.StrokeOp, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
	if sty.Miter > 0 {
		strokePathMiterJoin(sty, rhs, lhs, hw, pivot, n0, n1, r0, r1)
		return
@@ -512,7 +512,7 @@ func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po
	}
}

func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
func strokePathMiterJoin(sty clip.StrokeOp, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
	if n0 == n1.Mul(-1) {
		strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
		return
@@ -554,7 +554,7 @@ func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32
}

// strokePathCap caps the provided path qs, according to the provided stroke style sty.
func strokePathCap(sty clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
func strokePathCap(sty clip.StrokeOp, qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
	switch sty.Cap {
	case clip.FlatCap:
		strokePathFlatCap(qs, hw, pivot, n0)
diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
index b2c76ad..1cb828a 100644
--- a/internal/opconst/ops.go
+++ b/internal/opconst/ops.go
@@ -27,6 +27,10 @@ const (
	TypeAux
	TypeClip
	TypeProfile
	TypePath
	TypeOutline
	TypeStroke
	TypeDash
)

const (
@@ -47,8 +51,12 @@ const (
	TypePushLen           = 1
	TypePopLen            = 1
	TypeAuxLen            = 1
	TypeClipLen           = 1 + 4*4 + (4 + 2 + 4 + (4 + 1))
	TypeClipLen           = 1 + 4*4
	TypeProfileLen        = 1
	TypePathLen           = 1 + 4
	TypeOutlineLen        = 1
	TypeStrokeLen         = 1 + 4 + 4 + 1 + 1
	TypeDashLen           = 1 + 4 + 1
)

func (t OpType) Size() int {
@@ -72,6 +80,10 @@ func (t OpType) Size() int {
		TypeAuxLen,
		TypeClipLen,
		TypeProfileLen,
		TypePathLen,
		TypeOutlineLen,
		TypeStrokeLen,
		TypeDashLen,
	}[t-firstOpIndex]
}

diff --git a/internal/ops/reader.go b/internal/ops/reader.go
index 5376dc2..0bab3a0 100644
--- a/internal/ops/reader.go
+++ b/internal/ops/reader.go
@@ -98,12 +98,6 @@ func (r *Reader) Decode() (EncodedOp, bool) {
		refs = refs[r.pc.refs:]
		refs = refs[:nrefs]
		switch t {
		case opconst.TypeClip:
			// A Clip operation may have trailing dashes float32 data.
			// The last element of the fixed-length clip data is the number
			// of elements describing the dashes pattern.
			n += 4 * int(data[len(data)-1])
			data = data[:n]
		case opconst.TypeAux:
			// An Aux operations is always wrapped in a macro, and
			// its length is the remaining space.
diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go
index efc98b7..18faf4c 100644
--- a/internal/rendertest/clip_test.go
+++ b/internal/rendertest/clip_test.go
@@ -68,7 +68,10 @@ func TestPaintArc(t *testing.T) {
		p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi)
		p.Line(f32.Pt(0, -10))
		p.Line(f32.Pt(-50, 0))
		p.Outline().Add(o)
		p.Close()
		p.Op().Add(o)
		clip.OutlineOp{}.Add(o)
		clip.Op{}.Add(o)

		paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
	}, func(r result) {
@@ -87,7 +90,10 @@ func TestPaintAbsolute(t *testing.T) {
		p.MoveTo(f32.Pt(20, 20))
		p.LineTo(f32.Pt(80, 20))
		p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80))
		p.Outline().Add(o)
		p.Close()
		p.Op().Add(o)
		clip.OutlineOp{}.Add(o)
		clip.Op{}.Add(o)

		paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
	}, func(r result) {
@@ -125,13 +131,13 @@ func TestPaintClippedTexture(t *testing.T) {

func TestStrokedPathBevelFlat(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 2.5
		sty := clip.StrokeStyle{
			Cap:  clip.FlatCap,
			Join: clip.BevelJoin,
		}

		newStrokedPath(o).Stroke(width, sty).Add(o)
		newStrokedPath(o).Op().Add(o)
		clip.StrokeOp{
			Width: 2.5,
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
		}.Add(o)
		clip.Op{}.Add(o)

		paint.Fill(o, red)
	}, func(r result) {
@@ -142,13 +148,13 @@ func TestStrokedPathBevelFlat(t *testing.T) {

func TestStrokedPathBevelRound(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 2.5
		sty := clip.StrokeStyle{
			Cap:  clip.RoundCap,
			Join: clip.BevelJoin,
		}

		newStrokedPath(o).Stroke(width, sty).Add(o)
		newStrokedPath(o).Op().Add(o)
		clip.StrokeOp{
			Width: 2.5,
			Cap:   clip.RoundCap,
			Join:  clip.BevelJoin,
		}.Add(o)
		clip.Op{}.Add(o)

		paint.Fill(o, red)
	}, func(r result) {
@@ -159,13 +165,13 @@ func TestStrokedPathBevelRound(t *testing.T) {

func TestStrokedPathBevelSquare(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 2.5
		sty := clip.StrokeStyle{
			Cap:  clip.SquareCap,
			Join: clip.BevelJoin,
		}

		newStrokedPath(o).Stroke(width, sty).Add(o)
		newStrokedPath(o).Op().Add(o)
		clip.StrokeOp{
			Width: 2.5,
			Cap:   clip.SquareCap,
			Join:  clip.BevelJoin,
		}.Add(o)
		clip.Op{}.Add(o)

		paint.Fill(o, red)
	}, func(r result) {
@@ -176,13 +182,13 @@ func TestStrokedPathBevelSquare(t *testing.T) {

func TestStrokedPathRoundRound(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 2.5
		sty := clip.StrokeStyle{
			Cap:  clip.RoundCap,
			Join: clip.RoundJoin,
		}

		newStrokedPath(o).Stroke(width, sty).Add(o)
		newStrokedPath(o).Op().Add(o)
		clip.StrokeOp{
			Width: 2.5,
			Cap:   clip.RoundCap,
			Join:  clip.RoundJoin,
		}.Add(o)
		clip.Op{}.Add(o)

		paint.Fill(o, red)
	}, func(r result) {
@@ -193,18 +199,29 @@ func TestStrokedPathRoundRound(t *testing.T) {

func TestStrokedPathFlatMiter(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: 5,
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: 5,
			}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 2,
			}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}

		newZigZagPath(o).Stroke(width, sty).Add(o)
		paint.Fill(o, red)

		newZigZagPath(o).Stroke(2, clip.StrokeStyle{}).Add(o)
		paint.Fill(o, black)

	}, func(r result) {
		r.expect(0, 0, colornames.White)
@@ -215,18 +232,29 @@ func TestStrokedPathFlatMiter(t *testing.T) {

func TestStrokedPathFlatMiterInf(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: float32(math.Inf(+1)),
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: float32(math.Inf(+1)),
			}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 2,
			}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}

		newZigZagPath(o).Stroke(width, sty).Add(o)
		paint.Fill(o, red)

		newZigZagPath(o).Stroke(2, clip.StrokeStyle{}).Add(o)
		paint.Fill(o, black)

	}, func(r result) {
		r.expect(0, 0, colornames.White)
@@ -237,26 +265,32 @@ func TestStrokedPathFlatMiterInf(t *testing.T) {

func TestStrokedPathZeroWidth(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 2
		var sty clip.StrokeStyle
		{
			stk := op.Push(o)
			p := new(clip.Path)
			p.Begin(o)
			p.Move(f32.Pt(10, 50))
			p.Line(f32.Pt(50, 0))
			p.Stroke(width, sty).Add(o)
			p.Op().Add(o)
			clip.StrokeOp{Width: 2}.Add(o)

			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}

		{
			stk := op.Push(o)
			p := new(clip.Path)
			p.Begin(o)
			p.Move(f32.Pt(10, 50))
			p.Line(f32.Pt(30, 0))
			p.Stroke(0, sty).Add(o) // width=0, disable stroke
			p.Op().Add(o)
			clip.StrokeOp{}.Add(o) // width=0, disable stroke

			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}

	}, func(r result) {
@@ -269,25 +303,41 @@ func TestStrokedPathZeroWidth(t *testing.T) {

func TestDashedPathFlatCapEllipse(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: float32(math.Inf(+1)),
			Line: clip.DashStyle{
				Dashes: []float32{5, 3},
			},
		{
			stk := op.Push(o)
			newEllipsePath(o).Op().Add(o)
			var dash clip.Dash
			dash.Begin(o)
			dash.Dash(5)
			dash.Dash(3)
			dash.Op().Add(o)

			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: float32(math.Inf(+1)),
			}.Add(o)

			clip.Op{}.Add(o)

			paint.Fill(
				o,
				red,
			)
			stk.Pop()
		}
		{
			stk := op.Push(o)
			newEllipsePath(o).Op().Add(o)
			clip.StrokeOp{Width: 2}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(
				o,
				black,
			)
			stk.Pop()
		}
		paint.FillShape(
			o,
			colornames.Red,
			newEllipsePath(o).Stroke(width, sty),
		)
		paint.FillShape(
			o,
			colornames.Black,
			newEllipsePath(o).Stroke(2, clip.StrokeStyle{}),
		)

	}, func(r result) {
		r.expect(0, 0, colornames.White)
@@ -298,26 +348,33 @@ func TestDashedPathFlatCapEllipse(t *testing.T) {

func TestDashedPathFlatCapZ(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: float32(math.Inf(+1)),
			Line: clip.DashStyle{
				Dashes: []float32{5, 3},
			},
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			var dash clip.Dash
			dash.Begin(o)
			dash.Dash(5)
			dash.Dash(3)
			dash.Op().Add(o)
			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: float32(math.Inf(+1)),
			}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}
		paint.FillShape(
			o,
			colornames.Red,
			newZigZagPath(o).Stroke(width, sty),
		)
		paint.FillShape(
			o,
			colornames.Black,
			newZigZagPath(o).Stroke(2, clip.StrokeStyle{}),
		)

		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{Width: 2}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(40, 10, colornames.Black)
@@ -328,26 +385,31 @@ func TestDashedPathFlatCapZ(t *testing.T) {

func TestDashedPathFlatCapZNoDash(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: float32(math.Inf(+1)),
			Line: clip.DashStyle{
				Offset: 1,
			},
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: float32(math.Inf(+1)),
			}.Add(o)
			var dash clip.Dash
			dash.Begin(o)
			dash.Phase(1)
			dash.Op().Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{Width: 2}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}
		paint.FillShape(
			o,
			colornames.Red,
			newZigZagPath(o).Stroke(width, sty),
		)
		paint.FillShape(
			o,
			colornames.Black,
			newZigZagPath(o).Stroke(2, clip.StrokeStyle{}),
		)

	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(40, 10, colornames.Black)
@@ -358,26 +420,31 @@ func TestDashedPathFlatCapZNoDash(t *testing.T) {

func TestDashedPathFlatCapZNoPath(t *testing.T) {
	run(t, func(o *op.Ops) {
		const width = 10
		sty := clip.StrokeStyle{
			Cap:   clip.FlatCap,
			Join:  clip.BevelJoin,
			Miter: float32(math.Inf(+1)),
			Line: clip.DashStyle{
				Dashes: []float32{0},
			},
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{
				Width: 10,
				Cap:   clip.FlatCap,
				Join:  clip.BevelJoin,
				Miter: float32(math.Inf(+1)),
			}.Add(o)
			var dash clip.Dash
			dash.Begin(o)
			dash.Dash(0)
			dash.Op().Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, red)
			stk.Pop()
		}
		{
			stk := op.Push(o)
			newZigZagPath(o).Op().Add(o)
			clip.StrokeOp{Width: 2}.Add(o)
			clip.Op{}.Add(o)
			paint.Fill(o, black)
			stk.Pop()
		}
		paint.FillShape(
			o,
			colornames.Red,
			newZigZagPath(o).Stroke(width, sty),
		)
		paint.FillShape(
			o,
			colornames.Black,
			newZigZagPath(o).Stroke(2, clip.StrokeStyle{}),
		)

	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(40, 10, colornames.Black)
diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/internal/rendertest/refs/TestStrokedPathFlatMiter.png
index 95014b280da83d4b58cf51cbba059e88b1d21570..7ad77e3ac26cbe3742bff59c286895561d5dcecc 100644
GIT binary patch
delta 2246
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
U`oS2KBLDyZ07*qoM6N<$g0L}3aR2}S

delta 2297
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
T*FHTp00000NkvXXu0mjfH;Zu(

diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png
index d859637985a9bb897fa0cce522e98f75a1f3bba0..23a13c3b1013304c74b3b15320a9995aa346b9b8 100644
GIT binary patch
delta 2250
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
Y|Bc<h&s)CMr2qf`07*qoM6N<$f~1E|oB#j-

delta 2292
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
O0000<MNUMnLSTZnC1y_m

diff --git a/internal/rendertest/render_test.go b/internal/rendertest/render_test.go
index 7557615..f913b21 100644
--- a/internal/rendertest/render_test.go
+++ b/internal/rendertest/render_test.go
@@ -71,7 +71,10 @@ func TestRepeatedPaintsZ(t *testing.T) {
		builder.Line(f32.Pt(0, 10))
		builder.Line(f32.Pt(-10, 0))
		builder.Line(f32.Pt(0, -10))
		builder.Outline().Add(o)
		builder.Close()
		builder.Op().Add(o)
		clip.OutlineOp{}.Add(o)
		clip.Op{}.Add(o)
		paint.Fill(o, red)
	}, func(r result) {
		r.expect(5, 5, colornames.Red)
@@ -109,7 +112,10 @@ func constSqPath() op.CallOp {
	builder.Line(f32.Pt(0, 10))
	builder.Line(f32.Pt(-10, 0))
	builder.Line(f32.Pt(0, -10))
	builder.Outline().Add(innerOps)
	builder.Close()
	builder.Op().Add(innerOps)
	clip.OutlineOp{}.Add(innerOps)
	clip.Op{}.Add(innerOps)
	return m.Stop()
}

diff --git a/op/clip/clip.go b/op/clip/clip.go
index f33ef3a..7bc659e 100644
--- a/op/clip/clip.go
+++ b/op/clip/clip.go
@@ -26,11 +26,26 @@ type Path struct {
	pen     f32.Point
	macro   op.MacroOp
	start   f32.Point
	quads   uint32
}

// Pos returns the current pen position.
func (p *Path) Pos() f32.Point { return p.pen }

// PathOp sets the current path. Construct PathOps with Path.
type PathOp struct {
	call  op.CallOp
	quads uint32
}

func (op PathOp) Add(o *op.Ops) {
	data := o.Write(opconst.TypePathLen)
	data[0] = byte(opconst.TypePath)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], op.quads)
	op.call.Add(o)
}

// Op sets the current clip to the intersection of
// the existing clip with this clip.
//
@@ -39,29 +54,17 @@ func (p *Path) Pos() f32.Point { return p.pen }
type Op struct {
	call   op.CallOp
	bounds image.Rectangle
	width  float32     // Width of the stroked path, 0 for outline paths.
	style  StrokeStyle // Style of the stroked path, zero for outline paths.
}

func (p Op) Add(o *op.Ops) {
	p.call.Add(o)
	data := o.Write(opconst.TypeClipLen + len(p.style.Line.Dashes)*4)
	data := o.Write(opconst.TypeClipLen)
	data[0] = byte(opconst.TypeClip)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], uint32(p.bounds.Min.X))
	bo.PutUint32(data[5:], uint32(p.bounds.Min.Y))
	bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
	bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
	bo.PutUint32(data[17:], math.Float32bits(p.width))
	data[21] = uint8(p.style.Cap)
	data[22] = uint8(p.style.Join)
	bo.PutUint32(data[23:], math.Float32bits(p.style.Miter))
	bo.PutUint32(data[27:], math.Float32bits(p.style.Line.Offset))
	data[31] = uint8(len(p.style.Line.Dashes))
	dashes := data[32:]
	for i, v := range p.style.Line.Dashes {
		bo.PutUint32(dashes[i*4:], math.Float32bits(v))
	}
}

// Begin the path, storing the path data and final Op into ops.
@@ -126,6 +129,7 @@ func (p *Path) QuadTo(ctrl, to f32.Point) {
		To:   to,
	})
	p.pen = to
	p.quads++
}

// Arc adds an elliptical arc to the path. The implied ellipse is defined
@@ -332,34 +336,39 @@ func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Po
	return splits
}

// Outline closes the path and returns a clip operation that represents it.
func (p *Path) Outline() Op {
// Close closes the path.
func (p *Path) Close() {
	p.end()
}

// Op completes the path and sets the current path.
func (p *Path) Op() PathOp {
	c := p.macro.Stop()
	return Op{
		call: c,
	return PathOp{
		call:  c,
		quads: p.quads,
	}
}

// Stroke returns a stroked path with the specified width
// and configuration.
// If the provided width is <= 0, the path won't be stroked.
func (p *Path) Stroke(width float32, sty StrokeStyle) Op {
	if width <= 0 {
		// Explicitly discard the macro to ignore the path.
		p.macro.Stop()
		return Op{
			call: op.Record(p.ops).Stop(),
		}
	}

	c := p.macro.Stop()
	return Op{
		call:  c,
		width: width,
		style: sty,
	}
}
//func (p *Path) Stroke(width float32, sty StrokeStyle) Op {
//	if width <= 0 {
//		// Explicitly discard the macro to ignore the path.
//		p.macro.Stop()
//		return Op{
//			call: op.Record(p.ops).Stop(),
//		}
//	}
//
//	c := p.macro.Stop()
//	return Op{
//		call:  c,
//		width: width,
//		style: sty,
//	}
//}

// Rect represents the clip area of a pixel-aligned rectangle.
type Rect image.Rectangle
diff --git a/op/clip/shapes.go b/op/clip/shapes.go
index c5f551d..c93184a 100644
--- a/op/clip/shapes.go
+++ b/op/clip/shapes.go
@@ -36,7 +36,13 @@ func (rr RRect) Op(ops *op.Ops) Op {
	p.Begin(ops)
	p.Move(rr.Rect.Min)
	roundRect(&p, rr.Rect.Size(), rr.SE, rr.SW, rr.NW, rr.NE)
	return p.Outline()
	p.Close()

	p.Op().Add(ops)

	OutlineOp{}.Add(ops)

	return Op{}
}

// Add the rectangle clip.
@@ -49,7 +55,6 @@ type Border struct {
	// Rect is the bounds of the border.
	Rect  f32.Rectangle
	Width float32
	Style StrokeStyle
	// The corner radii.
	SE, SW, NW, NE float32
}
@@ -58,11 +63,13 @@ type Border struct {
func (b Border) Op(ops *op.Ops) Op {
	var p Path
	p.Begin(ops)

	p.Move(b.Rect.Min)
	roundRect(&p, b.Rect.Size(), b.SE, b.SW, b.NW, b.NE)
	p.Close()

	return p.Stroke(b.Width, b.Style)
	p.Op().Add(ops)
	StrokeOp{Width: b.Width}.Add(ops)
	return Op{}
}

// Add the border clip.
diff --git a/op/clip/stroke.go b/op/clip/stroke.go
index 7bf7929..fb38242 100644
--- a/op/clip/stroke.go
+++ b/op/clip/stroke.go
@@ -2,20 +2,13 @@

package clip

// StrokeStyle describes how a stroked path should be drawn.
// The zero value of StrokeStyle represents bevel-joined and flat-capped
// strokes with a solid line.
type StrokeStyle struct {
	Cap  StrokeCap
	Join StrokeJoin
import (
	"encoding/binary"
	"math"

	// Miter is the limit to apply to a miter joint.
	// The zero Miter disables the miter joint; setting Miter to +∞
	// unconditionally enables the miter joint.
	Miter float32

	Line DashStyle // Line defines the stroked path line.
}
	"gioui.org/internal/opconst"
	"gioui.org/op"
)

// StrokeCap describes the head or tail of a stroked path.
type StrokeCap uint8
@@ -47,9 +40,87 @@ const (
	RoundJoin
)

// DashStyle describes how a stroked path line should be drawn.
// DashStyle zero value draws a solid line.
type DashStyle struct {
	Offset float32   // Offset before the dash pattern.
	Dashes []float32 // Dashes is the sequence of lengths of [dash,space,...]
// StrokeOp applies a stroke to the current path.
type StrokeOp struct {
	Width float32 // Width of the stroked path, 0 for outline paths.

	// Miter is the limit to apply to a miter joint.
	// The zero Miter disables the miter joint; setting Miter to +∞
	// unconditionally enables the miter joint.
	Miter float32
	Cap   StrokeCap
	Join  StrokeJoin
}

func (op StrokeOp) Add(o *op.Ops) {
	data := o.Write(opconst.TypeStrokeLen)
	data[0] = byte(opconst.TypeStroke)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], math.Float32bits(op.Width))
	bo.PutUint32(data[5:], math.Float32bits(op.Miter))
	data[9] = uint8(op.Cap)
	data[10] = uint8(op.Join)
}

// OutlineOp fills the current path according to the non-zero
// winding rule.
type OutlineOp struct {
}

func (op OutlineOp) Add(o *op.Ops) {
	data := o.Write(opconst.TypeOutlineLen)
	data[0] = byte(opconst.TypeOutline)
}

// DashOp configures dashing for a stroked path. Construct a DashOp
// with Dashes and use it in combination with StrokeOp.
type DashOp struct {
	call  op.CallOp
	phase float32
	size  int // size of the pattern
}

func (op DashOp) Add(o *op.Ops) {
	data := o.Write(opconst.TypeDashLen)
	data[0] = byte(opconst.TypeDash)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], math.Float32bits(op.phase))
	data[5] = uint8(op.size) // FIXME(sbinet) uint16? uint32?
	op.call.Add(o)
}

// Dash records dashes' lengths and phase for a stroked path.
type Dash struct {
	ops   *op.Ops
	macro op.MacroOp
	phase float32
	size  int
}

func (d *Dash) Begin(ops *op.Ops) {
	d.ops = ops
	d.macro = op.Record(ops)
	// Write the TypeAux opcode
	data := ops.Write(opconst.TypeAuxLen)
	data[0] = byte(opconst.TypeAux)
}

func (d *Dash) Phase(v float32) {
	d.phase = v
}

func (d *Dash) Dash(length float32) {
	data := d.ops.Write(4)
	bo := binary.LittleEndian
	bo.PutUint32(data[0:], math.Float32bits(length))
	d.size++
}

func (d *Dash) Op() DashOp {
	c := d.macro.Stop()
	return DashOp{
		call:  c,
		phase: d.phase,
		size:  d.size,
	}
}
diff --git a/widget/material/loader.go b/widget/material/loader.go
index 48a4b6f..65efd5d 100644
--- a/widget/material/loader.go
+++ b/widget/material/loader.go
@@ -67,15 +67,13 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) {
		pen    = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius))
		center = f32.Pt(0, 0).Sub(pen)

		style = clip.StrokeStyle{
			Cap: clip.FlatCap,
		}

		p clip.Path
	)

	p.Begin(ops)
	p.Move(pen)
	p.Arc(center, center, delta)
	p.Stroke(width, style).Add(ops)
	p.Op().Add(ops)

	clip.StrokeOp{Width: width, Cap: clip.FlatCap}.Add(ops)
}
-- 
2.29.2
Reply to thread Export thread (mbox)