diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-03-03 15:23:25 -0500 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-03-03 15:23:25 -0500 |
| commit | 3ae1a7330b0eaab02fafd1f857c60fad1cd4fe19 (patch) | |
| tree | 069ba54c2ca7594e96cfe519acc3700a02a5d949 | |
| parent | 474f4984f8be450524eedd3e89bcbfdfa1b4f516 (diff) | |
| download | gui-3ae1a7330b0eaab02fafd1f857c60fad1cd4fe19.zip | |
lay/strain: represent rectangles as (origin, size)
| -rw-r--r-- | lay/strain/solve.go | 40 | ||||
| -rw-r--r-- | lay/strain/solve_test.go | 105 | ||||
| -rw-r--r-- | lay/strain/sym.go | 11 |
3 files changed, 131 insertions, 25 deletions
diff --git a/lay/strain/solve.go b/lay/strain/solve.go index 7fd3c03..71e427b 100644 --- a/lay/strain/solve.go +++ b/lay/strain/solve.go @@ -26,8 +26,7 @@ type Solver struct { // 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 + fields []SymRect // position and size of each field fieldSizeConstrs []sizeConstraint @@ -35,7 +34,7 @@ type Solver struct { layoutConstrs chan constrainRequest // constraints from the layout via AddConstraint() solveReqs chan solveRequest - style style.Style + style *style.Style ctx context.Context cancel func() @@ -75,24 +74,21 @@ type solveResponse struct { // // 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) { +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) + fields := make([]SymRect, nfields) for i, cs := range constraints { go tag.Tag(fieldConstrs, cs, func(c Constraint) int { return i }) - fieldOrigins[i] = NewSymPt() - fieldSizes[i] = NewSymPt() + fields[i] = NewSymRect() } ctx, cancel := context.WithCancel(context.Background()) solver := &Solver{ casso.NewSolver(), NewSymRect(), - fieldOrigins, - fieldSizes, + fields, make([]sizeConstraint, nfields), fieldConstrs, make(chan constrainRequest), @@ -101,7 +97,7 @@ func NewSolver(styl style.Style, constraints []<-chan Constraint) (*Solver, erro ctx, cancel, } - if err := editRect(solver.solver, solver.container, casso.Required); err != nil { + if err := editRect(solver.solver, solver.container, casso.Strong); err != nil { return nil, err } @@ -142,7 +138,7 @@ func (s *Solver) run() { // 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] + fieldSize := s.fields[fieldIdx].Size fieldConstrs := &s.fieldSizeConstrs[fieldIdx] // Clear mutually exclusive constraints and replace with new one @@ -223,16 +219,22 @@ func (s *Solver) solve(container image.Rectangle) (fields []image.Rectangle, err if err := suggestRect(s.solver, s.container, container); err != nil { return nil, err } - fields = make([]image.Rectangle, len(s.fieldOrigins)) + fields = make([]image.Rectangle, len(s.fields)) 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)))) + 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() } @@ -242,10 +244,8 @@ func (s *Solver) Close() { s.cancel() } 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] } +// position and size. +func (s *Solver) Field(i int) SymRect { return s.fields[i] } // AddConstraint imposes a constraint between two symbols. The symbols // may be aspects of the Container() or Field()s. diff --git a/lay/strain/solve_test.go b/lay/strain/solve_test.go new file mode 100644 index 0000000..61ab7e6 --- /dev/null +++ b/lay/strain/solve_test.go @@ -0,0 +1,105 @@ +package strain_test + +import ( + "testing" + "image" + "slices" + + "github.com/lithdew/casso" + + "github.com/faiface/gui/style" + "github.com/faiface/gui/lay/strain" +) + +type solverTest struct { + t *testing.T + *style.Style + *strain.Solver +} + +func newSolverTest(t *testing.T, constraints []<-chan strain.Constraint) solverTest { + styl, err := style.New() + if err != nil { + t.Fatal(err) + } + solver, err := strain.NewSolver(styl, constraints) + if err != nil { + t.Fatal(err) + } + return solverTest{t, styl, solver} +} + +func (st solverTest) Close() { + st.Solver.Close() + if err := st.Style.Close(); err != nil { + st.t.Error(err) + } +} + +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, wantFields []image.Rectangle) { + fields, err := st.Solver.Solve(container) + if err != nil { + st.t.Errorf("Solve(%v): %v; want %v", container, err, wantFields) + } + if !slices.Equal(fields, wantFields) { + st.t.Errorf("Solve(%v) = %v; want %v", container, fields, wantFields) + } +} + +// No constraints and zero-sized container. +func TestTrivial(t *testing.T) { + t.Parallel() + st := newSolverTest(t, nil) + defer st.Close() + fields, err := st.Solver.Solve(image.ZR) + if err != nil { + t.Error(err) + } + if len(fields) != 0 { + t.Errorf("expected 0 fields; got %d", len(fields)) + } +} + +// One field that occupies the whole container. +func TestSingleField(t *testing.T) { + // Setup + t.Parallel() + constraints := make(chan strain.Constraint) + st := newSolverTest(t, []<-chan strain.Constraint{constraints}) + defer st.Close() + defer close(constraints) + + // Add 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) + + // Solve + for _, container := range []image.Rectangle{ + image.ZR, + image.Rectangle{image.ZP, image.Pt(800, 600)}, + image.Rectangle{image.Pt(12, 34), image.Pt(123, 456)}, + } { + // field == container + st.solve(container, []image.Rectangle{container}) + } +} + +// Solver with only layout constaints, no field constraints. +func TestLayConstrs(t *testing.T) { + t.Parallel() + + st := newSolverTest(t, nil) + defer st.Close() + + t.Fail() // TODO +} diff --git a/lay/strain/sym.go b/lay/strain/sym.go index cf8777b..9d2a0bf 100644 --- a/lay/strain/sym.go +++ b/lay/strain/sym.go @@ -14,7 +14,8 @@ type SymPt struct { // SymRect is a set of Cassowary symbols representing an // image.Rectangle. type SymRect struct { - Min, Max SymPt + Origin SymPt // top-left corner position + Size SymPt // Dx() and Dy() } func NewSymPt() SymPt { @@ -28,10 +29,10 @@ func NewSymRect() SymRect { // editRect marks all symbols of a rectangle as editable with a // certain precedence. func editRect(solver *casso.Solver, sr SymRect, p casso.Priority) error { - if err := editPt(solver, sr.Min, p); err != nil { + if err := editPt(solver, sr.Origin, p); err != nil { return err } - if err := editPt(solver, sr.Max, p); err != nil { + if err := editPt(solver, sr.Size, p); err != nil { return err } return nil @@ -50,10 +51,10 @@ func editPt(solver *casso.Solver, sp SymPt, p casso.Priority) error { } func suggestRect(solver *casso.Solver, sr SymRect, r image.Rectangle) error { - if err := suggestPt(solver, sr.Min, r.Min); err != nil { + if err := suggestPt(solver, sr.Origin, r.Min); err != nil { return err } - if err := suggestPt(solver, sr.Max, r.Max); err != nil { + if err := suggestPt(solver, sr.Size, image.Pt(r.Dx(), r.Dy())); err != nil { return err } return nil |