diff options
| -rw-r--r-- | go.mod | 8 | ||||
| -rw-r--r-- | go.sum | 1 | ||||
| -rw-r--r-- | lay/strain/solve.go | 71 | ||||
| -rw-r--r-- | lay/strain/solve_test.go | 36 |
4 files changed, 88 insertions, 28 deletions
@@ -7,8 +7,14 @@ require ( github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 github.com/go-gl/glfw v0.0.0-20250301202403-da16c1255728 github.com/lithdew/casso v0.0.0-20200531104607-fe75aa82181f + github.com/stretchr/testify v1.5.1 golang.org/x/exp/shiny v0.0.0-20260218203240-3dfff04db8fa golang.org/x/image v0.36.0 ) -require golang.org/x/text v0.34.0 // indirect +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.34.0 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) @@ -19,6 +19,7 @@ golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lay/strain/solve.go b/lay/strain/solve.go index 89270ed..cfb1f41 100644 --- a/lay/strain/solve.go +++ b/lay/strain/solve.go @@ -12,6 +12,11 @@ import ( "github.com/faiface/gui/style" ) +const ( + fieldConstraintPriority = casso.Medium + layoutConstraintPriority = casso.Medium +) + // Solver uses the Cassowary algorithm to partition a rectangle among // several layout fields. // @@ -80,7 +85,7 @@ func NewSolver(styl *style.Style, constraints []<-chan Constraint) (*Solver, err solver := casso.NewSolver() container := NewSymRect() if err := editRect(solver, container, casso.Strong); err != nil { - return nil, err + return nil, fmt.Errorf("error marking container symbol as editable: %w", err) } s := &Solver{ @@ -96,7 +101,9 @@ func NewSolver(styl *style.Style, constraints []<-chan Constraint) (*Solver, err go func() { for tc := range fieldConstrs { constr, fieldIdx := tc.Val, tc.Tag - err := s.addSizeConstraint(constr, fieldIdx) + s.mu.Lock() + err := s.addFieldSizeConstraint(constr, fieldIdx) + s.mu.Unlock() if err != nil { log.Err.Printf("error adding layout constraint %#v from field %d: %v\n", constr, fieldIdx, err) @@ -104,19 +111,30 @@ func NewSolver(styl *style.Style, constraints []<-chan Constraint) (*Solver, err } }() + if err := s.addDefaultConstraints(); err != nil { + return nil, fmt.Errorf("error adding default constraint: %w", err) + } + return s, nil +} - // TODO: add some universal constraints: - // - field size <= container size - // - field origin >= container origin +func (s *Solver) addDefaultConstraints() error { + for _, field := range s.fields { + if err := s.AddConstraintPt(casso.GTE, field.Origin, s.container.Origin); err != nil { + return err + } + if err := s.AddConstraintPt(casso.LTE, field.Size, s.container.Size); err != nil { + return err + } + } + return nil } // addSizeConstraint adds or modifies a constraint on the size of a // field, removing mutually exclusive constraints. -func (s *Solver) addSizeConstraint(constr Constraint, i fieldIndex) error { - s.mu.Lock() - defer s.mu.Unlock() - +// +// Solver.mu must be held. +func (s *Solver) addFieldSizeConstraint(constr Constraint, i fieldIndex) error { fieldSize := s.fields[i].Size fieldConstrs := &s.fieldSizeConstrs[i] @@ -127,21 +145,21 @@ func (s *Solver) addSizeConstraint(constr Constraint, i fieldIndex) error { switch constr.Op { case casso.EQ: s.removeConstraints(fieldConstrs.widthEq, fieldConstrs.widthGte, fieldConstrs.widthLte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -width, fieldSize.X.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -width, fieldSize.X.T(1.0)) if err != nil { return err } fieldConstrs.widthEq = &c case casso.GTE: s.removeConstraints(fieldConstrs.widthEq, fieldConstrs.widthGte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -width, fieldSize.X.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -width, fieldSize.X.T(1.0)) if err != nil { return err } fieldConstrs.widthGte = &c case casso.LTE: s.removeConstraints(fieldConstrs.widthEq, fieldConstrs.widthLte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -width, fieldSize.X.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -width, fieldSize.X.T(1.0)) if err != nil { return err } @@ -154,21 +172,21 @@ func (s *Solver) addSizeConstraint(constr Constraint, i fieldIndex) error { switch constr.Op { case casso.EQ: s.removeConstraints(fieldConstrs.heightEq, fieldConstrs.heightGte, fieldConstrs.heightLte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -height, fieldSize.Y.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -height, fieldSize.Y.T(1.0)) if err != nil { return err } fieldConstrs.heightEq = &c case casso.GTE: s.removeConstraints(fieldConstrs.heightEq, fieldConstrs.heightGte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -height, fieldSize.Y.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -height, fieldSize.Y.T(1.0)) if err != nil { return err } fieldConstrs.heightGte = &c case casso.LTE: s.removeConstraints(fieldConstrs.heightEq, fieldConstrs.heightLte) - c, err := s.solver.AddConstraint(casso.NewConstraint(constr.Op, -height, fieldSize.Y.T(1.0))) + c, err := s.addFieldConstraint(constr.Op, -height, fieldSize.Y.T(1.0)) if err != nil { return err } @@ -183,6 +201,7 @@ func (s *Solver) addSizeConstraint(constr Constraint, i fieldIndex) error { return nil } +// Solver.mu must be held. func (s *Solver) removeConstraints(constrs ...*casso.Symbol) error { for _, constr := range constrs { if constr != nil { @@ -194,6 +213,13 @@ func (s *Solver) removeConstraints(constrs ...*casso.Symbol) error { return nil } +// Solver.mu must be held. +func (s *Solver) addFieldConstraint(op casso.Op, constant float64, terms ...casso.Term) (casso.Symbol, error) { + return s.solver.AddConstraintWithPriority( + fieldConstraintPriority, + casso.NewConstraint(op, constant, terms...)) +} + // Container returns the Cassowary symbols representing the layout // container's position and size. func (s *Solver) Container() SymRect { return s.container } @@ -209,10 +235,22 @@ func (s *Solver) Field(i int) SymRect { return s.fields[i] } func (s *Solver) AddConstraint(op casso.Op, lhs, rhs casso.Symbol) error { s.mu.Lock() defer s.mu.Unlock() - _, err := s.solver.AddConstraint(casso.NewConstraint(op, 0, lhs.T(1.0), rhs.T(-1.0))) + _, err := s.solver.AddConstraintWithPriority(layoutConstraintPriority, casso.NewConstraint(op, 0, lhs.T(1.0), rhs.T(-1.0))) return err } +// AddConstraintPt imposes a constraint between two point symbols: +// (lhs.X op rhs.X) and (lhs.Y op rhs.Y). +func (s *Solver) AddConstraintPt(op casso.Op, lhs, rhs SymPt) error { + if err := s.AddConstraint(op, lhs.X, rhs.X); err != nil { + return err + } + if err := s.AddConstraint(op, lhs.Y, rhs.Y); err != nil { + return err + } + return nil +} + // Solve uses the constraints that the Solver has received so far to // partition container, dividing it among the fields of the layout. // @@ -241,4 +279,5 @@ func (s *Solver) Solve(container image.Rectangle) ([]image.Rectangle, error) { return fields, nil } +// Solver.mu must be held. 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 d360c53..6281124 100644 --- a/lay/strain/solve_test.go +++ b/lay/strain/solve_test.go @@ -8,6 +8,7 @@ import ( "testing/synctest" "github.com/lithdew/casso" + "github.com/stretchr/testify/require" "golang.org/x/exp/shiny/unit" "golang.org/x/image/math/fixed" @@ -39,12 +40,6 @@ func (st solverTest) Close() { } } -func (st solverTest) addConstraint(op casso.Op, lhs, rhs casso.Symbol) { - if err := st.Solver.AddConstraint(op, lhs, rhs); err != nil { - st.t.Error(err) - } -} - func (st solverTest) solve(container image.Rectangle, validate func(fields []image.Rectangle) error) { fields, err := st.Solver.Solve(container) if err != nil { @@ -91,10 +86,8 @@ func TestSingleField(t *testing.T) { // Add layout constraints container := st.Solver.Container() field := st.Solver.Field(0) - st.addConstraint(casso.EQ, field.Origin.X, container.Origin.X) - st.addConstraint(casso.EQ, field.Origin.Y, container.Origin.Y) - st.addConstraint(casso.EQ, field.Size.X, container.Size.X) - st.addConstraint(casso.EQ, field.Size.Y, container.Size.Y) + require.NoError(t, st.Solver.AddConstraintPt(casso.EQ, field.Origin, container.Origin)) + require.NoError(t, st.Solver.AddConstraintPt(casso.EQ, field.Size, container.Size)) // Solve for _, container := range []image.Rectangle{ @@ -107,7 +100,7 @@ func TestSingleField(t *testing.T) { } } -// Widget gives its minimum size. +// Field gives its minimum size. func TestFieldMinSize(t *testing.T) { t.Parallel() synctest.Test(t, func(t *testing.T) { @@ -140,6 +133,27 @@ func TestFieldMinSize(t *testing.T) { }) } +// Field min size larger than container. +func TestFieldMinSizeLargerThanContainer(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + // Setup + constraints := make(chan strain.Constraint) + st := newSolverTest(t, []<-chan strain.Constraint{constraints}) + defer st.Close() + defer close(constraints) + + // Add widget constraints + constraints <- strain.Constraint{strain.Width, casso.GTE, unit.Value{200, unit.Px}} + constraints <- strain.Constraint{strain.Height, casso.GTE, unit.Value{300, unit.Px}} + synctest.Wait() + + // Solve + container := image.Rect(12, 34, 100, 200) + st.solve(container, validateEq([]image.Rectangle{container})) + }) +} + // Solver with only layout constaints, no field constraints. func TestLayConstrs(t *testing.T) { t.Parallel() |