aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-03-03 18:59:21 -0500
committerSam Anthony <sam@samanthony.xyz>2026-03-03 18:59:21 -0500
commit9a2c345cfd3697a5de3f5fd799f682ab822e3756 (patch)
tree3ab215fdf51b1ba82a335a432b946605d4a1468a
parentb3064e570fab92cb4fa6b4c9265be5a4cbcf80e4 (diff)
downloadgui-9a2c345cfd3697a5de3f5fd799f682ab822e3756.zip
lay/strain: add default constraints to Solver
-rw-r--r--go.mod8
-rw-r--r--go.sum1
-rw-r--r--lay/strain/solve.go71
-rw-r--r--lay/strain/solve_test.go36
4 files changed, 88 insertions, 28 deletions
diff --git a/go.mod b/go.mod
index 921713c..d98874d 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
index d8426d4..a5502ae 100644
--- a/go.sum
+++ b/go.sum
@@ -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()