aboutsummaryrefslogtreecommitdiffstats
path: root/lay/strain/solve.go
diff options
context:
space:
mode:
Diffstat (limited to 'lay/strain/solve.go')
-rw-r--r--lay/strain/solve.go276
1 files changed, 276 insertions, 0 deletions
diff --git a/lay/strain/solve.go b/lay/strain/solve.go
new file mode 100644
index 0000000..e1a89d8
--- /dev/null
+++ b/lay/strain/solve.go
@@ -0,0 +1,276 @@
+package strain
+
+import (
+ "context"
+ "fmt"
+ "image"
+
+ "github.com/lithdew/casso"
+
+ "github.com/faiface/gui/internal/log"
+ "github.com/faiface/gui/internal/tag"
+ "github.com/faiface/gui/style"
+)
+
+// Solver uses the Cassowary algorithm to partition a rectangle among
+// several layout fields.
+//
+// Solver uses constraints to control the position and size of fields.
+// There are two sources of constraints: 1) the layout, via
+// AddConstraint(); and 2) the fields, via the Constraint channels
+// passed to NewSolver(). The layout constraints control the position
+// 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
+
+ // External symbols
+ container SymRect // position and size of container
+ fieldOrigins []SymPt // top-left corner position of each field
+ fieldSizes []SymPt // width and height of each field
+
+ fieldSizeConstrs []sizeConstraint
+
+ 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()
+}
+
+// sizeConstraint sets upper/lower/exact bounds on the width and
+// height of a field.
+type sizeConstraint struct {
+ widthEq, widthGte, widthLte *casso.Symbol
+ heightEq, heightGte, heightLte *casso.Symbol
+
+ // These are the return values of casso.Solver.AddConstraint().
+ // Eq is mutually exclusive with both gte and lte. But gte and lte are
+ // not mutually exclusive (can have both a lower and upper bound).
+ // 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
+// 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])
+ fieldOrigins := make([]SymPt, nfields)
+ fieldSizes := make([]SymPt, nfields)
+ for i, cs := range constraints {
+ go tag.Tag(fieldConstrs, cs, func(c Constraint) int { return i })
+ fieldOrigins[i] = NewSymPt()
+ fieldSizes[i] = NewSymPt()
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ solver := &Solver{
+ casso.NewSolver(),
+ NewSymRect(),
+ fieldOrigins,
+ fieldSizes,
+ make([]sizeConstraint, nfields),
+ fieldConstrs,
+ make(chan constrainRequest),
+ make(chan solveRequest),
+ styl,
+ ctx,
+ cancel,
+ }
+ if err := editRect(solver.solver, solver.container, casso.Required); 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)
+
+ for {
+ select {
+ case tc := <-s.fieldConstrs: // constraint from a field, tagged by field index
+ constr, fieldIdx := tc.Val, tc.Tag
+ err := s.addSizeConstraint(constr, fieldIdx)
+ if err != nil {
+ 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}
+
+ case <-s.ctx.Done():
+ return
+ }
+ }
+}
+
+// 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.fieldSizes[fieldIdx]
+ fieldConstrs := &s.fieldSizeConstrs[fieldIdx]
+
+ // Clear mutually exclusive constraints and replace with new one
+ switch constr.Dim {
+ case Width:
+ width := s.style.Pixels(constr.Value).Round()
+ 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)))
+ 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)))
+ 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)))
+ if err != nil {
+ return err
+ }
+ fieldConstrs.widthLte = &c
+ default:
+ panic(fmt.Sprintf("unreachable: impossible %T: %v", constr.Op, constr.Op))
+ }
+ case Height:
+ height := s.style.Pixels(constr.Value).Round()
+ 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)))
+ 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)))
+ 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)))
+ if err != nil {
+ return err
+ }
+ fieldConstrs.heightLte = &c
+ default:
+ panic(fmt.Sprintf("unreachable: impossible %T: %v", constr.Op, constr.Op))
+ }
+ default:
+ panic(fmt.Sprintf("unreachable: impossible %T: %v", constr.Dim, constr.Dim))
+ }
+
+ return nil
+}
+
+func (s *Solver) removeConstraints(constrs ...*casso.Symbol) error {
+ for _, constr := range constrs {
+ if constr != nil {
+ if err := s.solver.RemoveConstraint(*constr); err != nil {
+ return err
+ }
+ }
+ }
+ 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.fieldOrigins))
+ for i := range fields {
+ origin, size := s.fieldOrigins[i], s.fieldSizes[i]
+ min := image.Pt(int(s.solver.Val(origin.X)), int(s.solver.Val(origin.Y)))
+ max := min.Add(image.Pt(int(s.solver.Val(size.X)), int(s.solver.Val(size.Y))))
+ fields[i] = image.Rectangle{min, max}
+ }
+ return fields, nil
+}
+
+// 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 }
+
+// Field returns the Cassowary symbols representing the i'th field's
+// position and size. origin represents the position of the top-left
+// corner of the field. size represents the width and height of the
+// field.
+func (s *Solver) Field(i int) (origin, size SymPt) { return s.fieldOrigins[i], s.fieldSizes[i] }
+
+// AddConstraint imposes a constraint between two symbols. The symbols
+// may be aspects of the Container() or Field()s.
+//
+// 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
+}
+
+// Solve uses the constraints that the Solver has received so far to
+// partition container, dividing it among the fields of the layout.
+//
+// It returns a slice of Rectangles, one per field. The slice has the
+// 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
+}