From 9a2c345cfd3697a5de3f5fd799f682ab822e3756 Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Tue, 3 Mar 2026 18:59:21 -0500 Subject: lay/strain: add default constraints to Solver --- lay/strain/solve.go | 71 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 16 deletions(-) (limited to 'lay/strain/solve.go') 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)) } -- cgit v1.2.3