mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
all: [API] split operation stack into per-state stacks
The op.Save and Load methods exist to support the need for
transformation, clip, pointer area state to behave as stacks. For
example, layout needs to apply an offset to its children but not
subsequent operations.
Before this change, op.Save and Load were used to save and restore the
state:
ops := new(op.Ops)
// Save state.
state := op.Save(ops)
// Apply offset.
op.Offset(...).Add(ops)
// Draw with offset applied.
draw(ops)
// Restore state.
state.Load()
A drawback with the op.Save mechanism is that there is no direct
connection between the state change and the saving and loading of state.
This causes confusion as to when a Save/Load is needed and who is
responsible for performing them, which leads to subtle bugs and over-use
of Save/Loads.
This change gets rid of the general state stack and replaces it with
per-state stacks. There is now a stack for transformation, clip, pointer
areas, and they can only be restored by the code pushing state to them.
The example above now becomes:
ops := new(op.Ops)
// Push offset to the transformation stack.
stack := op.Offset(...).Push(ops)
// Draw with offset applied.
draw(ops)
// Restore state.
stack.Pop()
For convenience, transformation also be Add'ed if the stack operation is
not required.
Simple state such as the current material no longer has a way to be
restored; it is assumed the client of a PaintOp adds their desired
material operation before it.
API change: replace op.Save/Load with explicit Push/Pop scopes for
op.TransformOps, pointer.AreaOps, clip.Ops.
To ease porting, this change retains a version of op.Save/Load that
saves and restores the transformation and clip stacks. It also retains
an Add method for clip.Op.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+37
-6
@@ -16,12 +16,6 @@ import (
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
var pathSeed maphash.Seed
|
||||
|
||||
func init() {
|
||||
pathSeed = maphash.MakeSeed()
|
||||
}
|
||||
|
||||
// Op represents a clip area. Op intersects the current clip area with
|
||||
// itself.
|
||||
type Op struct {
|
||||
@@ -32,7 +26,35 @@ type Op struct {
|
||||
dashes DashSpec
|
||||
}
|
||||
|
||||
// Stack represents an Op pushed on the clip stack.
|
||||
type Stack struct {
|
||||
ops *op.Ops
|
||||
id op.StackID
|
||||
macroID int
|
||||
}
|
||||
|
||||
var pathSeed maphash.Seed
|
||||
|
||||
func init() {
|
||||
pathSeed = maphash.MakeSeed()
|
||||
}
|
||||
|
||||
// Push saves the current clip state on the stack and updates the current
|
||||
// state to the intersection of the current p.
|
||||
func (p Op) Push(o *op.Ops) Stack {
|
||||
id, macroID := o.PushOp(op.ClipStack)
|
||||
p.add(o, true)
|
||||
return Stack{ops: o, id: id, macroID: macroID}
|
||||
}
|
||||
|
||||
// Add is like Push except it doesn't save the current state on the stack.
|
||||
//
|
||||
// Deprecated: use Push instead.
|
||||
func (p Op) Add(o *op.Ops) {
|
||||
p.add(o, false)
|
||||
}
|
||||
|
||||
func (p Op) add(o *op.Ops, push bool) {
|
||||
str := p.stroke
|
||||
path := p.path
|
||||
outline := p.outline
|
||||
@@ -76,6 +98,15 @@ func (p Op) Add(o *op.Ops) {
|
||||
if outline {
|
||||
data[17] = byte(1)
|
||||
}
|
||||
if push {
|
||||
data[18] = byte(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Stack) Pop() {
|
||||
s.ops.PopOp(op.ClipStack, s.id, s.macroID)
|
||||
data := s.ops.Write(opconst.TypePopClipLen)
|
||||
data[0] = byte(opconst.TypePopClip)
|
||||
}
|
||||
|
||||
func (p Op) approximateStroke(o *op.Ops) PathSpec {
|
||||
|
||||
@@ -20,3 +20,15 @@ func TestOpenPathOutlinePanic(t *testing.T) {
|
||||
p.Line(f32.Pt(10, 10))
|
||||
Outline{Path: p.End()}.Op()
|
||||
}
|
||||
|
||||
func TestTransformChecks(t *testing.T) {
|
||||
defer func() {
|
||||
if err := recover(); err == nil {
|
||||
t.Error("cross-macro Pop didn't panic")
|
||||
}
|
||||
}()
|
||||
var ops op.Ops
|
||||
st := Op{}.Push(&ops)
|
||||
op.Record(&ops)
|
||||
st.Pop()
|
||||
}
|
||||
|
||||
+5
-7
@@ -4,13 +4,11 @@
|
||||
Package clip provides operations for clipping paint operations.
|
||||
Drawing outside the current clip area is ignored.
|
||||
|
||||
The current clip is initially the infinite set. An Op sets the clip
|
||||
to the intersection of the current clip and the clip area it
|
||||
represents. If you need to reset the current clip to its value
|
||||
before applying an Op, use op.StackOp.
|
||||
The current clip is initially the infinite set. Pushing and Op sets the clip
|
||||
to the intersection of the current clip and pushed clip area. Popping the
|
||||
area restores the clip to its state before pushing.
|
||||
|
||||
General clipping areas are constructed with Path. Simpler special
|
||||
cases such as rectangular clip areas also exist as convenient
|
||||
constructors.
|
||||
General clipping areas are constructed with Path. Common cases such as
|
||||
rectangular clip areas also exist as convenient constructors.
|
||||
*/
|
||||
package clip
|
||||
|
||||
+24
-3
@@ -23,7 +23,14 @@ func (r Rect) Op() Op {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the clip operation.
|
||||
// Push the clip operation on the clip stack.
|
||||
func (r Rect) Push(ops *op.Ops) Stack {
|
||||
return r.Op().Push(ops)
|
||||
}
|
||||
|
||||
// Add the rectangle clip to the clip state.
|
||||
//
|
||||
// Deprecated: use Push instead.
|
||||
func (r Rect) Add(ops *op.Ops) {
|
||||
r.Op().Add(ops)
|
||||
}
|
||||
@@ -66,7 +73,14 @@ func (rr RRect) Op(ops *op.Ops) Op {
|
||||
return Outline{Path: rr.Path(ops)}.Op()
|
||||
}
|
||||
|
||||
// Add the rectangle clip.
|
||||
// Push the rectangle clip on the clip stack.
|
||||
func (rr RRect) Push(ops *op.Ops) Stack {
|
||||
return rr.Op(ops).Push(ops)
|
||||
}
|
||||
|
||||
// Add the rectangle clip to the clip state.
|
||||
//
|
||||
// Deprecated: use Push instead.
|
||||
func (rr RRect) Add(ops *op.Ops) {
|
||||
rr.Op(ops).Add(ops)
|
||||
}
|
||||
@@ -119,7 +133,14 @@ func (c Circle) Op(ops *op.Ops) Op {
|
||||
return Outline{Path: c.Path(ops)}.Op()
|
||||
}
|
||||
|
||||
// Add the circle clip.
|
||||
// Push the circle clip on the clip stack.
|
||||
func (c Circle) Push(ops *op.Ops) Stack {
|
||||
return c.Op(ops).Push(ops)
|
||||
}
|
||||
|
||||
// Add the circle clip to the clip state.
|
||||
//
|
||||
// Deprecated: use Push instead.
|
||||
func (c Circle) Add(ops *op.Ops) {
|
||||
c.Op(ops).Add(ops)
|
||||
}
|
||||
|
||||
@@ -30,24 +30,24 @@ Drawing a colored square:
|
||||
|
||||
State
|
||||
|
||||
An Ops list can be viewed as a very simple virtual machine: it has an implicit
|
||||
mutable state stack and execution flow can be controlled with macros.
|
||||
An Ops list can be viewed as a very simple virtual machine: it has state such
|
||||
as transformation and color and execution flow can be controlled with macros.
|
||||
|
||||
The Save function saves the current state for later restoring:
|
||||
Some state, such as the current color, is modified directly by operations with
|
||||
Add methods. Other state, such as transformation and clip shape, are
|
||||
represented by stacks.
|
||||
|
||||
This example sets the simple color state and pushes an offset to the
|
||||
transformation stack.
|
||||
|
||||
ops := new(op.Ops)
|
||||
// Save the current state, in particular the transform.
|
||||
state := op.Save(ops)
|
||||
// Apply a transform to subsequent operations.
|
||||
op.Offset(...).Add(ops)
|
||||
// Set the color.
|
||||
paint.ColorOp{...}.Add(ops)
|
||||
// Apply an offset to subsequent operations.
|
||||
stack := op.Offset(...).Push(ops)
|
||||
...
|
||||
// Restore the previous transform.
|
||||
state.Load()
|
||||
|
||||
You can also use this one-line to save the current state and restore it at the
|
||||
end of a function :
|
||||
|
||||
defer op.Save(ops).Load()
|
||||
// Undo the offset transformation.
|
||||
stack.Pop()
|
||||
|
||||
The MacroOp records a list of operations to be executed later:
|
||||
|
||||
@@ -67,6 +67,7 @@ package op
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
@@ -89,11 +90,20 @@ type Ops struct {
|
||||
nextStateID int
|
||||
|
||||
macroStack stack
|
||||
stacks [3]stack
|
||||
}
|
||||
|
||||
// StateOp represents a saved operation snapshop to be restored
|
||||
type StackKind uint8
|
||||
|
||||
const (
|
||||
ClipStack StackKind = iota
|
||||
AreaStack
|
||||
TransStack
|
||||
)
|
||||
|
||||
// stateOp represents a saved operation snapshop to be restored
|
||||
// later.
|
||||
type StateOp struct {
|
||||
type stateOp struct {
|
||||
id int
|
||||
macroID int
|
||||
ops *Ops
|
||||
@@ -102,7 +112,7 @@ type StateOp struct {
|
||||
// MacroOp records a list of operations for later use.
|
||||
type MacroOp struct {
|
||||
ops *Ops
|
||||
id stackID
|
||||
id StackID
|
||||
pc pc
|
||||
}
|
||||
|
||||
@@ -119,20 +129,27 @@ type InvalidateOp struct {
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// TransformOp applies a transform to the current transform. The zero value
|
||||
// for TransformOp represents the identity transform.
|
||||
// TransformOp represents a transformation that can be pushed on the
|
||||
// transformation stack.
|
||||
type TransformOp struct {
|
||||
t f32.Affine2D
|
||||
}
|
||||
|
||||
// stack tracks the integer identities of MacroOp
|
||||
// operations to ensure correct pairing of Record/End.
|
||||
// TransformStack represents a TransformOp pushed on the transformation stack.
|
||||
type TransformStack struct {
|
||||
id StackID
|
||||
macroID int
|
||||
ops *Ops
|
||||
}
|
||||
|
||||
// stack tracks the integer identities of stack operations to ensure correct
|
||||
// pairing of their push and pop methods.
|
||||
type stack struct {
|
||||
currentID int
|
||||
nextID int
|
||||
}
|
||||
|
||||
type stackID struct {
|
||||
type StackID struct {
|
||||
id int
|
||||
prev int
|
||||
}
|
||||
@@ -142,22 +159,21 @@ type pc struct {
|
||||
refs int
|
||||
}
|
||||
|
||||
// Defer executes c after all other operations have completed,
|
||||
// including previously deferred operations.
|
||||
// Defer saves the current transformation and restores it prior
|
||||
// to execution. All other operation state is reset.
|
||||
// Defer executes c after all other operations have completed, including
|
||||
// previously deferred operations.
|
||||
// Defer saves the transformation stack and pushes it prior to executing
|
||||
// c. All other operation state is reset.
|
||||
//
|
||||
// Note that deferred operations are executed in first-in-first-out
|
||||
// order, unlike the Go facility of the same name.
|
||||
// Note that deferred operations are executed in first-in-first-out order,
|
||||
// unlike the Go facility of the same name.
|
||||
func Defer(o *Ops, c CallOp) {
|
||||
if c.ops == nil {
|
||||
return
|
||||
}
|
||||
state := Save(o)
|
||||
state := save(o)
|
||||
// Wrap c in a macro that loads the saved state before execution.
|
||||
m := Record(o)
|
||||
load(o, opconst.InitialStateID, opconst.AllState)
|
||||
load(o, state.id, opconst.TransformState)
|
||||
state.load()
|
||||
c.Add(o)
|
||||
c = m.Stop()
|
||||
// A Defer is recorded as a TypeDefer followed by the
|
||||
@@ -167,52 +183,80 @@ func Defer(o *Ops, c CallOp) {
|
||||
c.Add(o)
|
||||
}
|
||||
|
||||
// Save the current operations state.
|
||||
func Save(o *Ops) StateOp {
|
||||
type SaveStack struct {
|
||||
ops *Ops
|
||||
clip struct {
|
||||
id StackID
|
||||
macroID int
|
||||
}
|
||||
trans TransformStack
|
||||
}
|
||||
|
||||
// Deprecated: use state-specific stack operations instead (TransformOp.Push
|
||||
// etc.).
|
||||
func Save(o *Ops) SaveStack {
|
||||
st := SaveStack{
|
||||
ops: o,
|
||||
trans: Offset(f32.Point{}).Push(o),
|
||||
}
|
||||
const inf = 1e6
|
||||
bounds := image.Rectangle{Min: image.Pt(-inf, -inf), Max: image.Pt(inf, inf)}
|
||||
{
|
||||
st.clip.id, st.clip.macroID = o.PushOp(ClipStack)
|
||||
// Push clip stack with no-op (infinite) clipping rect. Copied from clip.Op.Push.
|
||||
bo := binary.LittleEndian
|
||||
data := o.Write(opconst.TypeClipLen)
|
||||
data[0] = byte(opconst.TypeClip)
|
||||
bo.PutUint32(data[1:], uint32(bounds.Min.X))
|
||||
bo.PutUint32(data[5:], uint32(bounds.Min.Y))
|
||||
bo.PutUint32(data[9:], uint32(bounds.Max.X))
|
||||
bo.PutUint32(data[13:], uint32(bounds.Max.Y))
|
||||
data[17] = byte(1) // Outline
|
||||
data[18] = byte(1) // Push
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func (s SaveStack) Load() {
|
||||
// Pop clip.
|
||||
s.ops.PopOp(ClipStack, s.clip.id, s.clip.macroID)
|
||||
data := s.ops.Write(opconst.TypePopClipLen)
|
||||
data[0] = byte(opconst.TypePopClip)
|
||||
|
||||
s.trans.Pop()
|
||||
}
|
||||
|
||||
// save the effective transformation.
|
||||
func save(o *Ops) stateOp {
|
||||
o.nextStateID++
|
||||
s := StateOp{
|
||||
s := stateOp{
|
||||
ops: o,
|
||||
id: o.nextStateID,
|
||||
macroID: o.macroStack.currentID,
|
||||
}
|
||||
save(o, s.id)
|
||||
return s
|
||||
}
|
||||
|
||||
// save records a save of the operations state to
|
||||
// id.
|
||||
func save(o *Ops, id int) {
|
||||
bo := binary.LittleEndian
|
||||
data := o.Write(opconst.TypeSaveLen)
|
||||
data[0] = byte(opconst.TypeSave)
|
||||
bo.PutUint32(data[1:], uint32(id))
|
||||
}
|
||||
|
||||
// Load a previously saved operations state.
|
||||
func (s StateOp) Load() {
|
||||
if s.ops.macroStack.currentID != s.macroID {
|
||||
panic("load in a different macro than save")
|
||||
}
|
||||
if s.id == 0 {
|
||||
panic("zero-value op")
|
||||
}
|
||||
load(s.ops, s.id, opconst.AllState)
|
||||
bo.PutUint32(data[1:], uint32(s.id))
|
||||
return s
|
||||
}
|
||||
|
||||
// load a previously saved operations state given
|
||||
// its ID. Only state included in mask is affected.
|
||||
func load(o *Ops, id int, m opconst.StateMask) {
|
||||
// its ID.
|
||||
func (s stateOp) load() {
|
||||
bo := binary.LittleEndian
|
||||
data := o.Write(opconst.TypeLoadLen)
|
||||
data := s.ops.Write(opconst.TypeLoadLen)
|
||||
data[0] = byte(opconst.TypeLoad)
|
||||
data[1] = byte(m)
|
||||
bo.PutUint32(data[2:], uint32(id))
|
||||
bo.PutUint32(data[1:], uint32(s.id))
|
||||
}
|
||||
|
||||
// Reset the Ops, preparing it for re-use. Reset invalidates
|
||||
// any recorded macros.
|
||||
func (o *Ops) Reset() {
|
||||
o.macroStack = stack{}
|
||||
for i := range o.stacks {
|
||||
o.stacks[i] = stack{}
|
||||
}
|
||||
// Leave references to the GC.
|
||||
for i := range o.refs {
|
||||
o.refs[i] = nil
|
||||
@@ -244,6 +288,17 @@ func (o *Ops) Write(n int) []byte {
|
||||
return o.data[len(o.data)-n:]
|
||||
}
|
||||
|
||||
func (o *Ops) PushOp(kind StackKind) (StackID, int) {
|
||||
return o.stacks[kind].push(), o.macroStack.currentID
|
||||
}
|
||||
|
||||
func (o *Ops) PopOp(kind StackKind, sid StackID, macroID int) {
|
||||
if o.macroStack.currentID != macroID {
|
||||
panic("stack push and pop must not cross macro boundary")
|
||||
}
|
||||
o.stacks[kind].pop(sid)
|
||||
}
|
||||
|
||||
// Write1 is for internal use only.
|
||||
func (o *Ops) Write1(n int, ref1 interface{}) []byte {
|
||||
o.data = append(o.data, make([]byte, n)...)
|
||||
@@ -334,22 +389,45 @@ func Affine(a f32.Affine2D) TransformOp {
|
||||
return TransformOp{t: a}
|
||||
}
|
||||
|
||||
func (t TransformOp) Add(o *Ops) {
|
||||
data := o.Write(opconst.TypeTransformLen)
|
||||
data[0] = byte(opconst.TypeTransform)
|
||||
bo := binary.LittleEndian
|
||||
a, b, c, d, e, f := t.t.Elems()
|
||||
bo.PutUint32(data[1:], math.Float32bits(a))
|
||||
bo.PutUint32(data[1+4*1:], math.Float32bits(b))
|
||||
bo.PutUint32(data[1+4*2:], math.Float32bits(c))
|
||||
bo.PutUint32(data[1+4*3:], math.Float32bits(d))
|
||||
bo.PutUint32(data[1+4*4:], math.Float32bits(e))
|
||||
bo.PutUint32(data[1+4*5:], math.Float32bits(f))
|
||||
// Push the current transformation to the stack and then multiply the
|
||||
// current transformation with t.
|
||||
func (t TransformOp) Push(o *Ops) TransformStack {
|
||||
id, macroID := o.PushOp(TransStack)
|
||||
t.add(o, true)
|
||||
return TransformStack{ops: o, id: id, macroID: macroID}
|
||||
}
|
||||
|
||||
func (s *stack) push() stackID {
|
||||
// Add is like Push except it doesn't push the current transformation to the
|
||||
// stack.
|
||||
func (t TransformOp) Add(o *Ops) {
|
||||
t.add(o, false)
|
||||
}
|
||||
|
||||
func (t TransformOp) add(o *Ops, push bool) {
|
||||
data := o.Write(opconst.TypeTransformLen)
|
||||
data[0] = byte(opconst.TypeTransform)
|
||||
if push {
|
||||
data[1] = 1
|
||||
}
|
||||
bo := binary.LittleEndian
|
||||
a, b, c, d, e, f := t.t.Elems()
|
||||
bo.PutUint32(data[2:], math.Float32bits(a))
|
||||
bo.PutUint32(data[2+4*1:], math.Float32bits(b))
|
||||
bo.PutUint32(data[2+4*2:], math.Float32bits(c))
|
||||
bo.PutUint32(data[2+4*3:], math.Float32bits(d))
|
||||
bo.PutUint32(data[2+4*4:], math.Float32bits(e))
|
||||
bo.PutUint32(data[2+4*5:], math.Float32bits(f))
|
||||
}
|
||||
|
||||
func (t TransformStack) Pop() {
|
||||
t.ops.PopOp(TransStack, t.id, t.macroID)
|
||||
data := t.ops.Write(opconst.TypePopTransformLen)
|
||||
data[0] = byte(opconst.TypePopTransform)
|
||||
}
|
||||
|
||||
func (s *stack) push() StackID {
|
||||
s.nextID++
|
||||
sid := stackID{
|
||||
sid := StackID{
|
||||
id: s.nextID,
|
||||
prev: s.currentID,
|
||||
}
|
||||
@@ -357,13 +435,13 @@ func (s *stack) push() stackID {
|
||||
return sid
|
||||
}
|
||||
|
||||
func (s *stack) check(sid stackID) {
|
||||
func (s *stack) check(sid StackID) {
|
||||
if s.currentID != sid.id {
|
||||
panic("unbalanced operation")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) pop(sid stackID) {
|
||||
func (s *stack) pop(sid StackID) {
|
||||
s.check(sid)
|
||||
s.currentID = sid.prev
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package op
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
)
|
||||
|
||||
func TestTransformChecks(t *testing.T) {
|
||||
defer func() {
|
||||
if err := recover(); err == nil {
|
||||
t.Error("cross-macro Pop didn't panic")
|
||||
}
|
||||
}()
|
||||
var ops Ops
|
||||
trans := Offset(f32.Point{}).Push(&ops)
|
||||
Record(&ops)
|
||||
trans.Pop()
|
||||
}
|
||||
+1
-3
@@ -136,8 +136,7 @@ func (d PaintOp) Add(o *op.Ops) {
|
||||
|
||||
// FillShape fills the clip shape with a color.
|
||||
func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) {
|
||||
defer op.Save(ops).Load()
|
||||
shape.Add(ops)
|
||||
defer shape.Push(ops).Pop()
|
||||
Fill(ops, c)
|
||||
}
|
||||
|
||||
@@ -146,7 +145,6 @@ func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) {
|
||||
// the painted area. Use FillShape unless you need to paint several
|
||||
// times within the same clip.Op.
|
||||
func Fill(ops *op.Ops, c color.NRGBA) {
|
||||
defer op.Save(ops).Load()
|
||||
ColorOp{Color: c}.Add(ops)
|
||||
PaintOp{}.Add(ops)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user