aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-03-03 16:43:56 -0500
committerSam Anthony <sam@samanthony.xyz>2026-03-03 16:43:56 -0500
commit483236742ddcd7883b5f9cff92244129274aa79c (patch)
tree2ba1613b9b8b6f84264ce0cc3df497a0bfe893fb
parent3ae1a7330b0eaab02fafd1f857c60fad1cd4fe19 (diff)
downloadgui-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.go197
-rw-r--r--lay/strain/solve_test.go60
-rw-r--r--lay/strain/sym.go2
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 {