diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-03-03 16:43:56 -0500 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-03-03 16:43:56 -0500 |
| commit | 483236742ddcd7883b5f9cff92244129274aa79c (patch) | |
| tree | 2ba1613b9b8b6f84264ce0cc3df497a0bfe893fb | |
| parent | 3ae1a7330b0eaab02fafd1f857c60fad1cd4fe19 (diff) | |
| download | gui-483236742ddcd7883b5f9cff92244129274aa79c.zip | |
lay/strain: simplify Solver internal synchronization
Replaced the AddConstraint() and Solve() methods' request/reply
channels, used a mutex instread.
Solver now closes the fieldConstrs channel automatically; removed the
Close() method.
Generally tidied up.
| -rw-r--r-- | lay/strain/solve.go | 197 | ||||
| -rw-r--r-- | lay/strain/solve_test.go | 60 | ||||
| -rw-r--r-- | lay/strain/sym.go | 2 |
3 files changed, 135 insertions, 124 deletions
diff --git a/lay/strain/solve.go b/lay/strain/solve.go index 71e427b..620998f 100644 --- a/lay/strain/solve.go +++ b/lay/strain/solve.go @@ -1,9 +1,9 @@ package strain import ( - "context" "fmt" "image" + "sync" "github.com/lithdew/casso" @@ -22,27 +22,26 @@ import ( // and size of fields within the container, while the field // constraints control the width and height of each field. type Solver struct { - solver *casso.Solver + solver *casso.Solver + style *style.Style + fieldConstrs chan tag.Tagged[Constraint, fieldIndex] // incoming constraints from fields // External symbols - container SymRect // position and size of container - fields []SymRect // position and size of each field + container SymRect // position and size of container + fields []SymRect // position and size of each field - fieldSizeConstrs []sizeConstraint + // Constraint ID symbols + fieldSizeConstrs []sizeConstraintSymbols - fieldConstrs chan tag.Tagged[Constraint, int] // constraints from fields, tagged by index - layoutConstrs chan constrainRequest // constraints from the layout via AddConstraint() - solveReqs chan solveRequest - - style *style.Style - - ctx context.Context - cancel func() + mu sync.Mutex } -// sizeConstraint sets upper/lower/exact bounds on the width and -// height of a field. -type sizeConstraint struct { +type fieldIndex int + +// sizeConstraintSymbols is a set of constraint ID symbols that +// control the upper/lower/exact bounds on the width and height of a +// field. +type sizeConstraintSymbols struct { widthEq, widthGte, widthLte *casso.Symbol heightEq, heightGte, heightLte *casso.Symbol @@ -52,94 +51,75 @@ type sizeConstraint struct { // Nil means no constraint. } -type constrainRequest struct { - constr casso.Constraint - res chan<- error -} - -type solveRequest struct { - container image.Rectangle - res chan<- solveResponse -} - -type solveResponse struct { - fields []image.Rectangle - err error -} - // NewSolver creates a Solver that can be used to resolve constraints -// received from the given channels; one channel per field in the +// received from the given channels: one channel per field in the // layout. These are generally the receiving side of some Envs' // Impose() channels. -// -// The Solver should be closed after use, but not before all of the -// constraint channels have been closed. func NewSolver(styl *style.Style, constraints []<-chan Constraint) (*Solver, error) { nfields := len(constraints) - - fieldConstrs := make(chan tag.Tagged[Constraint, int]) fields := make([]SymRect, nfields) + fieldConstrs := make(chan tag.Tagged[Constraint, fieldIndex]) + var wg sync.WaitGroup + wg.Add(nfields) for i, cs := range constraints { - go tag.Tag(fieldConstrs, cs, func(c Constraint) int { return i }) fields[i] = NewSymRect() + go func() { + // Tag incoming field constraints by field index and multiplex them into fieldConstrs + tag.Tag(fieldConstrs, cs, func(c Constraint) fieldIndex { + return fieldIndex(i) + }) + wg.Done() + }() } - ctx, cancel := context.WithCancel(context.Background()) - solver := &Solver{ - casso.NewSolver(), - NewSymRect(), - fields, - make([]sizeConstraint, nfields), - fieldConstrs, - make(chan constrainRequest), - make(chan solveRequest), - styl, - ctx, - cancel, - } - if err := editRect(solver.solver, solver.container, casso.Strong); err != nil { + go func() { + wg.Wait() // once all fields close their constraints channels, + close(fieldConstrs) + }() + + solver := casso.NewSolver() + container := NewSymRect() + if err := editRect(solver, container, casso.Strong); err != nil { return nil, err } - go solver.run() - - return solver, nil -} - -func (s *Solver) run() { - defer close(s.fieldConstrs) - defer close(s.layoutConstrs) - defer close(s.solveReqs) + s := &Solver{ + solver, + styl, + fieldConstrs, + container, + fields, + make([]sizeConstraintSymbols, nfields), + sync.Mutex{}, + } - for { - select { - case tc := <-s.fieldConstrs: // constraint from a field, tagged by field index + go func() { + for tc := range fieldConstrs { constr, fieldIdx := tc.Val, tc.Tag err := s.addSizeConstraint(constr, fieldIdx) if err != nil { + // TODO: run the solver in a golang.org/x/sync/errgroup rather than just logging errors. log.Err.Printf("error adding layout constraint %#v from field %d: %v\n", constr, fieldIdx, err) } + } + }() - case req := <-s.layoutConstrs: // constraint from the layout via AddConstraint() - _, err := s.solver.AddConstraint(req.constr) - req.res <- err - - case req := <-s.solveReqs: - fields, err := s.solve(req.container) - req.res <- solveResponse{fields, err} + return s, nil - case <-s.ctx.Done(): - return - } - } + // TODO: add some universal constraints: + // - field size <= container size + // - field origin >= container origin } // addSizeConstraint adds or modifies a constraint on the size of a // field, removing mutually exclusive constraints. -func (s *Solver) addSizeConstraint(constr Constraint, fieldIdx int) error { - fieldSize := s.fields[fieldIdx].Size - fieldConstrs := &s.fieldSizeConstrs[fieldIdx] +func (s *Solver) addSizeConstraint(constr Constraint, i fieldIndex) error { + s.mu.Lock() + defer s.mu.Unlock() + + fieldSize := s.fields[i].Size + fieldConstrs := &s.fieldSizeConstrs[i] // Clear mutually exclusive constraints and replace with new one switch constr.Dimension { @@ -215,30 +195,6 @@ func (s *Solver) removeConstraints(constrs ...*casso.Symbol) error { return nil } -func (s *Solver) solve(container image.Rectangle) (fields []image.Rectangle, err error) { - if err := suggestRect(s.solver, s.container, container); err != nil { - return nil, err - } - fields = make([]image.Rectangle, len(s.fields)) - for i := range fields { - field := s.fields[i] - min := image.Pt( - s.intVal(field.Origin.X), - s.intVal(field.Origin.Y)) - max := min.Add(image.Pt( - s.intVal(field.Size.X), - s.intVal(field.Size.Y))) - fields[i] = image.Rectangle{min, max} - } - return fields, nil -} - -func (s *Solver) intVal(sym casso.Symbol) int { return int(s.solver.Val(sym)) } - -// Close destroys the solver. You must first close all the Constraint -// channels that were passed to NewSolver() before calling Close. -func (s *Solver) Close() { s.cancel() } - // Container returns the Cassowary symbols representing the layout // container's position and size. func (s *Solver) Container() SymRect { return s.container } @@ -252,13 +208,10 @@ func (s *Solver) Field(i int) SymRect { return s.fields[i] } // // Once added, a constraint cannot be removed or modified. func (s *Solver) AddConstraint(op casso.Op, lhs, rhs casso.Symbol) error { - res := make(chan error) - defer close(res) - s.layoutConstrs <- constrainRequest{ - casso.NewConstraint(op, 0, lhs.T(1.0), rhs.T(-1.0)), - res, - } - return <-res + s.mu.Lock() + defer s.mu.Unlock() + _, err := s.solver.AddConstraint(casso.NewConstraint(op, 0, lhs.T(1.0), rhs.T(-1.0))) + return err } // Solve uses the constraints that the Solver has received so far to @@ -268,9 +221,25 @@ func (s *Solver) AddConstraint(op casso.Op, lhs, rhs casso.Symbol) error { // same length as the slice of Constraint channels that were passed to // NewSolver(). func (s *Solver) Solve(container image.Rectangle) ([]image.Rectangle, error) { - resc := make(chan solveResponse) - defer close(resc) - s.solveReqs <- solveRequest{container, resc} - res := <-resc - return res.fields, res.err + s.mu.Lock() + defer s.mu.Unlock() + + if err := suggestRect(s.solver, s.container, container); err != nil { + return nil, err + } + + fields := make([]image.Rectangle, len(s.fields)) + for i := range fields { + field := s.fields[i] + min := image.Pt( + s.intVal(field.Origin.X), + s.intVal(field.Origin.Y)) + max := min.Add(image.Pt( + s.intVal(field.Size.X), + s.intVal(field.Size.Y))) + fields[i] = image.Rectangle{min, max} + } + return fields, nil } + +func (s *Solver) intVal(sym casso.Symbol) int { return int(s.solver.Val(sym)) } diff --git a/lay/strain/solve_test.go b/lay/strain/solve_test.go index 61ab7e6..a2cec87 100644 --- a/lay/strain/solve_test.go +++ b/lay/strain/solve_test.go @@ -1,14 +1,17 @@ package strain_test import ( - "testing" + "fmt" "image" "slices" + "testing" "github.com/lithdew/casso" + "golang.org/x/exp/shiny/unit" + "golang.org/x/image/math/fixed" - "github.com/faiface/gui/style" "github.com/faiface/gui/lay/strain" + "github.com/faiface/gui/style" ) type solverTest struct { @@ -30,7 +33,6 @@ func newSolverTest(t *testing.T, constraints []<-chan strain.Constraint) solverT } func (st solverTest) Close() { - st.Solver.Close() if err := st.Style.Close(); err != nil { st.t.Error(err) } @@ -42,13 +44,22 @@ func (st solverTest) addConstraint(op casso.Op, lhs, rhs casso.Symbol) { } } -func (st solverTest) solve(container image.Rectangle, wantFields []image.Rectangle) { +func (st solverTest) solve(container image.Rectangle, validate func(fields []image.Rectangle) error) { fields, err := st.Solver.Solve(container) if err != nil { - st.t.Errorf("Solve(%v): %v; want %v", container, err, wantFields) + st.t.Errorf("Solve(%v): %v", container, err) + } + if err := validate(fields); err != nil { + st.t.Errorf("Solve(%v) = %v; %v", container, fields, err) } - if !slices.Equal(fields, wantFields) { - st.t.Errorf("Solve(%v) = %v; want %v", container, fields, wantFields) +} + +func validateEq(wantFields []image.Rectangle) func(fields []image.Rectangle) error { + return func(fields []image.Rectangle) error { + if !slices.Equal(fields, wantFields) { + return fmt.Errorf("want %v", wantFields) + } + return nil } } @@ -75,7 +86,7 @@ func TestSingleField(t *testing.T) { defer st.Close() defer close(constraints) - // Add constraints + // Add layout constraints container := st.Solver.Container() field := st.Solver.Field(0) st.addConstraint(casso.EQ, field.Origin.X, container.Origin.X) @@ -90,7 +101,7 @@ func TestSingleField(t *testing.T) { image.Rectangle{image.Pt(12, 34), image.Pt(123, 456)}, } { // field == container - st.solve(container, []image.Rectangle{container}) + st.solve(container, validateEq([]image.Rectangle{container})) } } @@ -103,3 +114,34 @@ func TestLayConstrs(t *testing.T) { t.Fail() // TODO } + +// Widget gives its minimum size. +func TestFieldMinSize(t *testing.T) { + t.Parallel() + + // Setup + constraints := make(chan strain.Constraint) + st := newSolverTest(t, []<-chan strain.Constraint{constraints}) + defer st.Close() + defer close(constraints) + + // Add widget constraints + minWidth := unit.Value{32, unit.Ch} + minHeight := unit.Value{1.5, unit.Em} + constraints <- strain.Constraint{strain.Width, casso.GTE, minWidth} + constraints <- strain.Constraint{strain.Height, casso.GTE, minHeight} + + // Solve + st.solve(image.Rect(12, 34, 800, 600), func(fields []image.Rectangle) error { + if len(fields) != 1 { + return fmt.Errorf("got %d fields; want %d", len(fields), 1) + } + field := fields[0] + if fixed.I(field.Dx()) < st.Style.Pixels(minWidth) { + return fmt.Errorf("dx = %v; want >= %v", field.Dx(), st.Style.Pixels(minWidth)) + } else if fixed.I(field.Dy()) < st.Style.Pixels(minHeight) { + return fmt.Errorf("dy = %v; want >= %v", field.Dy(), st.Style.Pixels(minHeight)) + } + return nil + }) +} diff --git a/lay/strain/sym.go b/lay/strain/sym.go index 9d2a0bf..63f4f11 100644 --- a/lay/strain/sym.go +++ b/lay/strain/sym.go @@ -15,7 +15,7 @@ type SymPt struct { // image.Rectangle. type SymRect struct { Origin SymPt // top-left corner position - Size SymPt // Dx() and Dy() + Size SymPt // Dx() and Dy() } func NewSymPt() SymPt { |