okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Ayman Bagabasfix: lint issuesf678006

main
28.8 KiB973 linesraw
1// Package layout partitions terminal screen space into rectangular regions
2// using a constraint-based solver.
3//
4// Under the hood, it relies on the [Cassowary constraint solver algorithm] to
5// resolve competing size requirements. Each constraint carries a priority, so
6// the solver can pick the best trade-off when not every requirement fits.
7//
8// # How It Works
9//
10// A [Layout] takes the available area and a list of constraints ([Len], [Ratio],
11// [Percent], [Fill], [Min], [Max]) and produces a set of non-overlapping rectangles.
12// The solver tries to honour every constraint; when that is impossible it
13// relaxes lower-priority ones first.
14//
15// You are not required to use [Layout] at all. If you prefer manual control,
16// you can compute [uv.Rectangle] values yourself with plain arithmetic.
17//
18// # Acknowledgements
19//
20// This implementation is heavily based on [Ratatui] source code and
21// is roughly 1:1 translation from Rust with some minor API adjustments.
22//
23// [Cassowary constraint solver algorithm]: https://en.wikipedia.org/wiki/Cassowary_(software)
24// [Ratatui]: https://ratatui.rs/
25package layout
26
27import (
28	"fmt"
29	"hash/fnv"
30	"math"
31
32	uv "github.com/charmbracelet/ultraviolet"
33	"github.com/charmbracelet/ultraviolet/internal/casso"
34)
35
36// floatPrecisionMultiplier scales cell positions into a higher-precision
37// floating-point domain before handing them to the constraint solver.
38// The number of trailing zeros determines the decimal precision kept
39// during rounding.
40const floatPrecisionMultiplier float64 = 100.0
41
42const (
43	required casso.Priority = 1_001_001_000
44	strong   casso.Priority = 1_000_000
45	medium   casso.Priority = 1_000
46	weak     casso.Priority = 1
47)
48
49const (
50	// spacerSizeEq enforces equal sizing across spacers.
51	//
52	// 	┌     ┐┌───┐┌     ┐┌───┐┌     ┐
53	// 	  ==x  │   │  ==x  │   │  ==x
54	// 	└     ┘└───┘└     ┘└───┘└     ┘
55	spacerSizeEq = required / 10.0
56
57	// minSizeGTE enforces the lower-bound inequality for [Min] constraints.
58	//
59	// 	┌────────┐
60	// 	│Min(>=x)│
61	// 	└────────┘
62	minSizeGTE = strong * 100.0
63
64	// maxSizeLTE enforces the upper-bound inequality for [Max] constraints.
65	//
66	// 	┌────────┐
67	// 	│Max(<=x)│
68	// 	└────────┘
69	maxSizeLTE = strong * 100.0
70
71	// lengthSizeEq pins the segment to the exact size requested by a [Len] constraint.
72	//
73	// 	┌────────┐
74	// 	│Len(==x)│
75	// 	└────────┘
76	lengthSizeEq = strong * 10.0
77
78	// percentSizeEq tries to make the segment match its [Percent] target.
79	//
80	// 	┌────────────┐
81	// 	│Percent(==x)│
82	// 	└────────────┘
83	percentSizeEq = strong
84
85	// ratioSizeEq tries to make the segment match its [Ratio] target.
86	//
87	// 	┌────────────┐
88	// 	│Ratio(==x,y)│
89	// 	└────────────┘
90	ratioSizeEq = strong / 10.0
91
92	// minSizeEq is an equality companion for the [Min] lower-bound; it
93	// nudges the segment toward the minimum value when room is tight.
94	//
95	// 	┌────────┐
96	// 	│Min(==x)│
97	// 	└────────┘
98	minSizeEq = medium * 10.0
99
100	// maxSizeEq is an equality companion for the [Max] upper-bound;
101	// it nudges the segment toward the maximum value.
102	//
103	// 	┌────────┐
104	// 	│Max(==x)│
105	// 	└────────┘
106	maxSizeEq = medium * 10.0
107
108	// fillGrow lets [Fill] segments expand into available space.
109	//
110	// 	┌─────────────────────┐
111	// 	│<=     Fill(x)     =>│
112	// 	└─────────────────────┘
113	fillGrow = medium
114
115	// grow is a general expansion priority (used by [Min] in non-legacy flex).
116	//
117	// 	┌────────────┐
118	// 	│<= Min(x) =>│
119	// 	└────────────┘
120	grow casso.Priority = 100.0
121
122	// spaceGrow allows spacers to expand and absorb remaining room.
123	//
124	// 	┌       ┐
125	// 	 <= x =>
126	// 	└       ┘
127	spaceGrow = weak * 10.0
128
129	// allSegmentGrow encourages all segments to share the same size.
130	//
131	// 	┌───────┐
132	// 	│<= x =>│
133	// 	└───────┘
134	allSegmentGrow = weak
135)
136
137// Splitted holds the rectangles produced by a [Layout.Split] call.
138type Splitted []uv.Rectangle
139
140// Assign stores each resulting rectangle into the corresponding pointer.
141//
142// Nil pointers are silently skipped.
143//
144// Panics when len(areas) exceeds the number of rectangles in [Splitted].
145//
146// # Examples
147//
148//	var top, bottom uv.Rectangle
149//
150//	layout.New(layout.Fill(1), layout.Len(1)).
151//		Split(area).
152//	    Assign(&top, &bottom)
153func (s Splitted) Assign(areas ...*uv.Rectangle) {
154	for i := range areas {
155		if areas[i] != nil {
156			*areas[i] = s[i]
157		}
158	}
159}
160
161// Direction controls whether a [Layout] arranges its segments
162// horizontally (left to right) or vertically (top to bottom).
163type Direction int
164
165const (
166	// DirectionVertical - layout segments are arranged top to bottom (default).
167	DirectionVertical Direction = iota
168	// DirectionHorizontal - layout segments are arranged side by side (left to right).
169	DirectionHorizontal
170)
171
172// New returns a [Layout] configured with the given direction and constraints.
173func New(direction Direction, constraints ...Constraint) Layout {
174	return Layout{
175		Direction:   direction,
176		Constraints: constraints,
177	}
178}
179
180// Vertical is shorthand for New(DirectionVertical, constraints...).
181func Vertical(constraints ...Constraint) Layout {
182	return New(DirectionVertical, constraints...)
183}
184
185// Horizontal is shorthand for New(DirectionHorizontal, constraints...).
186func Horizontal(constraints ...Constraint) Layout {
187	return New(DirectionHorizontal, constraints...)
188}
189
190// Layout splits a rectangular area into smaller rectangles using a set of constraints.
191// It is the primary building block for structuring terminal user interfaces.
192//
193// Fields:
194//   - Direction: whether segments flow vertically or horizontally.
195//   - Constraints: the sizing rules ([Len], [Ratio], [Percent], [Fill], [Min], [Max]).
196//   - Padding: inset applied to the outer area before solving.
197//   - Flex: strategy for distributing leftover space among segments.
198//   - Spacing: gap (or overlap, if negative) between adjacent segments.
199//
200// Internally, sizes are resolved by a Cassowary linear-constraint solver that
201// satisfies as many rules as it can, preferring higher-priority constraints
202// when trade-offs are necessary.
203type Layout struct {
204	Direction   Direction
205	Constraints []Constraint
206	Padding     Padding
207	// Spacing is the gap between adjacent segments, measured in cells.
208	// A negative value causes segments to overlap by that many cells.
209	Spacing int
210	Flex    Flex
211}
212
213// WithDirection returns a shallow copy of the layout using the specified direction.
214func (l Layout) WithDirection(direction Direction) Layout {
215	l.Direction = direction
216
217	return l
218}
219
220// WithPadding returns a shallow copy of the layout using the specified padding.
221func (l Layout) WithPadding(padding Padding) Layout {
222	l.Padding = padding
223	return l
224}
225
226// WithFlex returns a shallow copy of the layout using the specified flex strategy.
227func (l Layout) WithFlex(flex Flex) Layout {
228	l.Flex = flex
229
230	return l
231}
232
233// WithSpacing returns a shallow copy of the layout using the specified spacing value.
234func (l Layout) WithSpacing(spacing int) Layout {
235	l.Spacing = spacing
236
237	return l
238}
239
240// WithConstraints returns a shallow copy of the layout with the given
241// constraints appended to its existing list.
242func (l Layout) WithConstraints(constraints ...Constraint) Layout {
243	l.Constraints = append(l.Constraints, constraints...)
244
245	return l
246}
247
248// SplitWithSpacers divides the given area into content segments and the
249// gaps (spacers) between them. It returns both slices; use [Layout.Split]
250// if you only need the content rectangles.
251func (l Layout) SplitWithSpacers(area uv.Rectangle) (segments, spacers Splitted) {
252	segments, spacers, err := l.splitCached(area)
253	if err != nil {
254		panic(err)
255	}
256
257	return segments, spacers
258}
259
260// Split partitions the area into content rectangles according to the
261// layout's direction and constraints.
262//
263// Because every constraint is evaluated against the total area, mixing
264// relative constraints (Percent, Ratio) with absolute ones (Min, Max, Len)
265// can produce ambiguous results. For example, splitting 100 cells as
266// [Min(20), Percent(50), Percent(50)] will not necessarily yield [20, 40, 40].
267func (l Layout) Split(area uv.Rectangle) Splitted {
268	segments, _ := l.SplitWithSpacers(area)
269
270	return segments
271}
272
273func (l Layout) splitCached(area uv.Rectangle) (segments, spacers []uv.Rectangle, err error) {
274	globalCacheMu.Lock()
275	defer globalCacheMu.Unlock()
276
277	key := l.cacheKey(area)
278
279	if v, ok := globalCache.Get(key); ok {
280		return v.Segments, v.Spacers, nil
281	}
282
283	segments, spacers, err = l.split(area)
284	if err != nil {
285		return nil, nil, err
286	}
287
288	globalCache.Add(key, cacheValue{Segments: segments, Spacers: spacers})
289
290	return segments, spacers, nil
291}
292
293func (l Layout) split(area uv.Rectangle) (segments, spacers []uv.Rectangle, err error) {
294	s := casso.NewSolver()
295
296	innerArea := l.Padding.apply(area)
297
298	var areaStart, areaEnd float64
299
300	switch l.Direction {
301	case DirectionHorizontal:
302		areaStart = float64(innerArea.Min.X) * floatPrecisionMultiplier
303		areaEnd = float64(innerArea.Max.X) * floatPrecisionMultiplier
304
305	case DirectionVertical:
306		areaStart = float64(innerArea.Min.Y) * floatPrecisionMultiplier
307		areaEnd = float64(innerArea.Max.Y) * floatPrecisionMultiplier
308	}
309
310	// 	<───────────────────────────────────area_size──────────────────────────────────>
311	// 	┌─area_start                                                          area_end─┐
312	// 	V                                                                              V
313	// 	┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐
314	// 	│    │                   │    │                   │    │                   │    │
315	// 	V    V                   V    V                   V    V                   V    V
316	// 	┌   ┐┌──────────────────┐┌   ┐┌──────────────────┐┌   ┐┌──────────────────┐┌   ┐
317	// 	     │     Max(20)      │     │      Max(20)     │     │      Max(20)     │
318	// 	└   ┘└──────────────────┘└   ┘└──────────────────┘└   ┘└──────────────────┘└   ┘
319	// 	^    ^                   ^    ^                   ^    ^                   ^    ^
320	// 	│    │                   │    │                   │    │                   │    │
321	// 	└─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┶━━━━━━━━━┳━━━━━━━━━┵─┬──┘
322	// 	  │            ┃           │            ┃           │            ┃           │
323	// 	  └────────────╂───────────┴────────────╂───────────┴────────────╂──Spacers──┘
324	// 	               ┃                        ┃                        ┃
325	// 	               ┗━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━Segments━━━━━━━━┛
326
327	variableCount := len(l.Constraints)*2 + 2
328
329	variables := make([]casso.Symbol, variableCount)
330	for i := range variableCount {
331		variables[i] = casso.New()
332	}
333
334	spacerElements := newElements(variables)
335	segmentElements := newElements(variables[1:])
336
337	spacing := l.Spacing
338
339	areaEl := element{
340		start: variables[0],
341		end:   variables[len(variables)-1],
342	}
343
344	if err := configureArea(s, areaEl, areaStart, areaEnd); err != nil {
345		return nil, nil, fmt.Errorf("configure area: %w", err)
346	}
347
348	if err := configureVariableInAreaConstraints(s, variables, areaEl); err != nil {
349		return nil, nil, fmt.Errorf("configure variable in area constraints: %w", err)
350	}
351
352	if err := configureVariableConstraints(s, variables); err != nil {
353		return nil, nil, fmt.Errorf("configure variable constraints: %w", err)
354	}
355
356	if err := configureFlexConstraints(s, areaEl, spacerElements, l.Flex, spacing); err != nil {
357		return nil, nil, fmt.Errorf("configure flex constraints: %w", err)
358	}
359
360	if err := configureConstraints(s, areaEl, segmentElements, l.Constraints, l.Flex); err != nil {
361		return nil, nil, fmt.Errorf("configure constraints: %w", err)
362	}
363
364	if err := configureFillConstraints(s, segmentElements, l.Constraints, l.Flex); err != nil {
365		return nil, nil, fmt.Errorf("configure fill constraints: %w", err)
366	}
367
368	if l.Flex != FlexLegacy {
369		for i := 0; i < len(segmentElements)-1; i++ {
370			left := segmentElements[i]
371			right := segmentElements[i+1]
372
373			if _, err := s.Add(allSegmentGrow, left.sizeEqSize(right)); err != nil {
374				return nil, nil, fmt.Errorf("add has size constraint: %w", err)
375			}
376		}
377	}
378
379	changes := make(map[casso.Symbol]float64, variableCount)
380
381	for _, v := range variables {
382		changes[v] = s.Val(v)
383	}
384
385	segments = changesToRects(changes, segmentElements, innerArea, l.Direction)
386	spacers = changesToRects(changes, spacerElements, innerArea, l.Direction)
387
388	return segments, spacers, nil
389}
390
391func (l Layout) cacheKey(area uv.Rectangle) cacheKey {
392	h := fnv.New64a()
393
394	for _, c := range l.Constraints {
395		c.hash(h)
396	}
397
398	return cacheKey{
399		Area:            area,
400		Direction:       l.Direction,
401		ConstraintsHash: h.Sum64(),
402		Padding:         l.Padding,
403		Spacing:         l.Spacing,
404		Flex:            l.Flex,
405	}
406}
407
408func changesToRects(
409	changes map[casso.Symbol]float64,
410	elements []element,
411	area uv.Rectangle,
412	direction Direction,
413) []uv.Rectangle {
414	var rects []uv.Rectangle
415
416	for _, e := range elements {
417		startVal := changes[e.start]
418		endVal := changes[e.end]
419
420		startRounded := int(math.Round(math.Round(startVal) / floatPrecisionMultiplier))
421		endRounded := int(math.Round(math.Round(endVal) / floatPrecisionMultiplier))
422
423		size := max(0, endRounded-startRounded)
424
425		switch direction {
426		case DirectionHorizontal:
427			rect := uv.Rect(startRounded, area.Min.Y, size, area.Dy())
428
429			rects = append(rects, rect)
430
431		case DirectionVertical:
432			rect := uv.Rect(area.Min.X, startRounded, area.Dx(), size)
433
434			rects = append(rects, rect)
435		}
436	}
437
438	return rects
439}
440
441// configureFillConstraints ensures that every [Fill] (and, outside legacy mode,
442// every [Min]) segment grows proportionally to its scaling factor, so that
443// remaining space is shared according to the declared weights.
444//
445//	[Fill(1), Fill(1)]
446//	┌──────┐┌──────┐
447//	│abcdef││abcdef│
448//	└──────┘└──────┘
449//
450//	[Fill(1), Fill(2)]
451//	┌──────┐┌────────────┐
452//	│abcdef││abcdefabcdef│
453//	└──────┘└────────────┘
454//
455//	size == base_element * scaling_factor
456func configureFillConstraints(
457	s *casso.Solver,
458	segments []element,
459	constraints []Constraint,
460	flex Flex,
461) error {
462	var (
463		validConstraints []Constraint
464		validSegments    []element
465	)
466
467	for i := 0; i < min(len(constraints), len(segments)); i++ {
468		c := constraints[i]
469		seg := segments[i]
470
471		switch c.(type) {
472		case Fill, Min:
473			if _, ok := c.(Min); ok && flex == FlexLegacy {
474				continue
475			}
476
477			validConstraints = append(validConstraints, c)
478			validSegments = append(validSegments, seg)
479		}
480	}
481
482	for _, indices := range combinations(len(validConstraints), 2) {
483		i, j := indices[0], indices[1]
484
485		leftConstraint := validConstraints[i]
486		leftSegment := validSegments[i]
487
488		rightConstraint := validConstraints[j]
489		rightSegment := validSegments[j]
490
491		getScalingFactor := func(c Constraint) float64 {
492			var scalingFactor float64
493
494			switch c := c.(type) {
495			case Fill:
496				scale := float64(c)
497
498				scalingFactor = 1e-6
499				scalingFactor = max(scalingFactor, scale)
500
501			case Min:
502				scalingFactor = 1
503			}
504
505			return scalingFactor
506		}
507
508		leftScalingFactor := getScalingFactor(leftConstraint)
509		rightScalingFactor := getScalingFactor(rightConstraint)
510
511		c := casso.NewConstraint(casso.EQ, 0,
512			leftSegment.end.T(rightScalingFactor),
513			leftSegment.start.T(-rightScalingFactor),
514			rightSegment.end.T(-leftScalingFactor),
515			rightSegment.start.T(leftScalingFactor),
516		)
517
518		if _, err := s.Add(grow, c); err != nil {
519			return fmt.Errorf("add constraint: %w", err)
520		}
521	}
522
523	return nil
524}
525
526func configureConstraints(
527	s *casso.Solver,
528	area element,
529	segments []element,
530	constraints []Constraint,
531	flex Flex,
532) error {
533	for i := 0; i < min(len(constraints), len(segments)); i++ {
534		constraint := constraints[i]
535		segment := segments[i]
536
537		switch constraint := constraint.(type) {
538		case Max:
539			size := int(constraint)
540
541			if _, err := s.Add(maxSizeLTE, segment.sizeLTE(size)); err != nil {
542				return fmt.Errorf("add constraints: %w", err)
543			}
544
545			if _, err := s.Add(maxSizeEq, segment.sizeEqConst(size)); err != nil {
546				return fmt.Errorf("add constraints: %w", err)
547			}
548
549		case Min:
550			size := int(constraint)
551
552			if _, err := s.Add(minSizeGTE, segment.sizeGTE(size)); err != nil {
553				return fmt.Errorf("add has min size constraint: %w", err)
554			}
555
556			if flex == FlexLegacy {
557				if _, err := s.Add(minSizeEq, segment.sizeEqConst(size)); err != nil {
558					return fmt.Errorf("add has size constraint: %w", err)
559				}
560			} else {
561				if _, err := s.Add(fillGrow, segment.sizeEqSize(area)); err != nil {
562					return fmt.Errorf("add has size constraint: %w", err)
563				}
564			}
565
566		case Len:
567			length := int(constraint)
568
569			if _, err := s.Add(lengthSizeEq, segment.sizeEqConst(length)); err != nil {
570				return fmt.Errorf("add has int size constraint: %w", err)
571			}
572
573		case Percent:
574			f := float64(constraint) / 100
575
576			if _, err := s.Add(percentSizeEq, segment.sizeEqScaledSize(area, f)); err != nil {
577				return fmt.Errorf("add has size constraint: %w", err)
578			}
579
580		case Ratio:
581			f := float64(constraint.Num) / float64(max(1, constraint.Den))
582
583			if _, err := s.Add(ratioSizeEq, segment.sizeEqScaledSize(area, f)); err != nil {
584				return fmt.Errorf("add has size constraint: %w", err)
585			}
586
587		case Fill:
588			if _, err := s.Add(fillGrow, segment.sizeEqSize(area)); err != nil {
589				return fmt.Errorf("add has size constraint: %w", err)
590			}
591		}
592	}
593
594	return nil
595}
596
597func configureFlexConstraints(
598	s *casso.Solver,
599	area element,
600	spacers []element,
601	flex Flex,
602	spacing int,
603) error {
604	var spacersExceptFirstAndLast []element
605
606	if len(spacers) > 2 {
607		spacersExceptFirstAndLast = spacers[1 : len(spacers)-1]
608	}
609
610	switch flex {
611	case FlexLegacy:
612		for _, sp := range spacersExceptFirstAndLast {
613			if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil {
614				return fmt.Errorf("add has size constraint: %w", err)
615			}
616		}
617
618		if len(spacers) >= 2 {
619			first, last := spacers[0], spacers[len(spacers)-1]
620
621			if _, err := s.Add(required-weak, first.empty()); err != nil {
622				return fmt.Errorf("add constraints: %w", err)
623			}
624
625			if _, err := s.Add(required-weak, last.empty()); err != nil {
626				return fmt.Errorf("add constraints: %w", err)
627			}
628		}
629
630	case FlexSpaceEvenly:
631		for _, indices := range combinations(len(spacers), 2) {
632			i, j := indices[0], indices[1]
633
634			left, right := spacers[i], spacers[j]
635
636			if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil {
637				return fmt.Errorf("add has size constraint: %w", err)
638			}
639		}
640
641		for _, sp := range spacers {
642			if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil {
643				return fmt.Errorf("add constraints: %w", err)
644			}
645
646			if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil {
647				return fmt.Errorf("add constraints: %w", err)
648			}
649		}
650
651	case FlexSpaceAround:
652		if len(spacers) <= 2 {
653			for _, indices := range combinations(len(spacers), 2) {
654				i, j := indices[0], indices[1]
655
656				left, right := spacers[i], spacers[j]
657
658				if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil {
659					return fmt.Errorf("add has size constraint: %w", err)
660				}
661			}
662
663			for _, sp := range spacers {
664				if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil {
665					return fmt.Errorf("add constraints: %w", err)
666				}
667
668				if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil {
669					return fmt.Errorf("add constraints: %w", err)
670				}
671			}
672		} else {
673			first, rest := spacers[0], spacers[1:]
674			last, middle := rest[len(rest)-1], rest[:len(rest)-1]
675
676			for _, indices := range combinations(len(middle), 2) {
677				i, j := indices[0], indices[1]
678
679				left, right := middle[i], middle[j]
680
681				if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil {
682					return fmt.Errorf("add has size constraint: %w", err)
683				}
684			}
685
686			if len(middle) > 0 {
687				firstMiddle := middle[0]
688
689				for _, e := range []element{first, last} {
690					if _, err := s.Add(spacerSizeEq, firstMiddle.sizeEqDouble(e)); err != nil {
691						return fmt.Errorf("add has double size constraint: %w", err)
692					}
693				}
694			}
695
696			for _, sp := range spacers {
697				if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil {
698					return fmt.Errorf("add has min size constraint: %w", err)
699				}
700
701				if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil {
702					return fmt.Errorf("add has size constraint: %w", err)
703				}
704			}
705		}
706
707	case FlexSpaceBetween:
708		for _, indices := range combinations(len(spacersExceptFirstAndLast), 2) {
709			i, j := indices[0], indices[1]
710
711			left, right := spacersExceptFirstAndLast[i], spacersExceptFirstAndLast[j]
712
713			if _, err := s.Add(spacerSizeEq, left.sizeEqSize(right)); err != nil {
714				return fmt.Errorf("add has size constraint: %w", err)
715			}
716		}
717
718		for _, sp := range spacersExceptFirstAndLast {
719			if _, err := s.Add(spacerSizeEq, sp.sizeGTE(spacing)); err != nil {
720				return fmt.Errorf("add constraints: %w", err)
721			}
722
723			if _, err := s.Add(spaceGrow, sp.sizeEqSize(area)); err != nil {
724				return fmt.Errorf("add constraints: %w", err)
725			}
726		}
727
728		if len(spacers) >= 2 {
729			first, last := spacers[0], spacers[len(spacers)-1]
730
731			if _, err := s.Add(required-weak, first.empty()); err != nil {
732				return fmt.Errorf("add constraints: %w", err)
733			}
734
735			if _, err := s.Add(required-weak, last.empty()); err != nil {
736				return fmt.Errorf("add constraints: %w", err)
737			}
738		}
739
740	case FlexStart:
741		for _, sp := range spacersExceptFirstAndLast {
742			if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil {
743				return fmt.Errorf("add has size constraint: %w", err)
744			}
745		}
746
747		if len(spacers) >= 2 {
748			first := spacers[0]
749			last := spacers[len(spacers)-1]
750
751			if _, err := s.Add(required-weak, first.empty()); err != nil {
752				return fmt.Errorf("add constraints: %w", err)
753			}
754
755			if _, err := s.Add(grow, last.sizeEqSize(area)); err != nil {
756				return fmt.Errorf("add constraints: %w", err)
757			}
758		}
759
760	case FlexCenter:
761		for _, sp := range spacersExceptFirstAndLast {
762			if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil {
763				return fmt.Errorf("add has size constraint: %w", err)
764			}
765		}
766
767		if len(spacers) >= 2 {
768			first, last := spacers[0], spacers[len(spacers)-1]
769
770			if _, err := s.Add(grow, first.sizeEqSize(area)); err != nil {
771				return fmt.Errorf("add constraints: %w", err)
772			}
773
774			if _, err := s.Add(grow, last.sizeEqSize(area)); err != nil {
775				return fmt.Errorf("add constraints: %w", err)
776			}
777
778			if _, err := s.Add(spacerSizeEq, first.sizeEqSize(last)); err != nil {
779				return fmt.Errorf("add constraints: %w", err)
780			}
781		}
782
783	case FlexEnd:
784		for _, sp := range spacersExceptFirstAndLast {
785			if _, err := s.Add(spacerSizeEq, sp.sizeEqConst(spacing)); err != nil {
786				return fmt.Errorf("add has size constraint: %w", err)
787			}
788		}
789
790		if len(spacers) >= 2 {
791			first := spacers[0]
792			last := spacers[len(spacers)-1]
793
794			if _, err := s.Add(required-weak, last.empty()); err != nil {
795				return fmt.Errorf("add constraints: %w", err)
796			}
797
798			if _, err := s.Add(grow, first.sizeEqSize(area)); err != nil {
799				return fmt.Errorf("add constraints: %w", err)
800			}
801		}
802	}
803
804	return nil
805}
806
807func configureVariableConstraints(
808	s *casso.Solver,
809	variables []casso.Symbol,
810) error {
811	// 	┌────┬───────────────────┬────┬─────variables─────┬────┬───────────────────┬────┐
812	// 	│    │                   │    │                   │    │                   │    │
813	// 	v    v                   v    v                   v    v                   v    v
814	// 	┌   ┐┌──────────────────┐┌   ┐┌──────────────────┐┌   ┐┌──────────────────┐┌   ┐
815	// 	     │     Max(20)      │     │      Max(20)     │     │      Max(20)     │
816	// 	└   ┘└──────────────────┘└   ┘└──────────────────┘└   ┘└──────────────────┘└   ┘
817	// 	^    ^                   ^    ^                   ^    ^                   ^    ^
818	// 	└v0  └v1                 └v2  └v3                 └v4  └v5                 └v6  └v7
819
820	variables = variables[1:]
821
822	count := len(variables)
823
824	for i := 0; i < count-count%2; i += 2 {
825		left, right := variables[i], variables[i+1]
826
827		if _, err := s.Add(required, casso.NewConstraint(casso.LTE, 0, left.T(1), right.T(-1))); err != nil {
828			return fmt.Errorf("add constraint: %w", err)
829		}
830	}
831
832	return nil
833}
834
835func configureVariableInAreaConstraints(
836	s *casso.Solver,
837	variables []casso.Symbol,
838	area element,
839) error {
840	for _, v := range variables {
841		if _, err := s.Add(required, casso.NewConstraint(casso.GTE, 0, v.T(1), area.start.T(-1))); err != nil {
842			return fmt.Errorf("add start constraint: %w", err)
843		}
844
845		if _, err := s.Add(required, casso.NewConstraint(casso.LTE, 0, v.T(1), area.end.T(-1))); err != nil {
846			return fmt.Errorf("add end constraint: %w", err)
847		}
848	}
849
850	return nil
851}
852
853func configureArea(
854	s *casso.Solver,
855	area element,
856	areaStart, areaEnd float64,
857) error {
858	if _, err := s.Add(required, casso.NewConstraint(casso.EQ, -areaStart, area.start.T(1))); err != nil {
859		return fmt.Errorf("add start constraint: %w", err)
860	}
861
862	if _, err := s.Add(required, casso.NewConstraint(casso.EQ, -areaEnd, area.end.T(1))); err != nil {
863		return fmt.Errorf("add end constraint: %w", err)
864	}
865
866	return nil
867}
868
869func newElements(variables []casso.Symbol) []element {
870	count := len(variables)
871
872	elements := make([]element, 0, count/2+1)
873
874	for i := 0; i < count-count%2; i += 2 {
875		s, e := variables[i], variables[i+1]
876
877		elements = append(elements, element{start: s, end: e})
878	}
879
880	return elements
881}
882
883type element struct {
884	start, end casso.Symbol
885}
886
887func (e element) empty() casso.Constraint {
888	return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1))
889}
890
891func (e element) sizeEqConst(size int) casso.Constraint {
892	return casso.NewConstraint(casso.EQ, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1))
893}
894
895func (e element) sizeLTE(size int) casso.Constraint {
896	return casso.NewConstraint(casso.LTE, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1))
897}
898
899func (e element) sizeGTE(size int) casso.Constraint {
900	return casso.NewConstraint(casso.GTE, -float64(size)*floatPrecisionMultiplier, e.end.T(1), e.start.T(-1))
901}
902
903func (e element) sizeEqSize(other element) casso.Constraint {
904	return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-1), other.start.T(1))
905}
906
907func (e element) sizeEqScaledSize(other element, f float64) casso.Constraint {
908	return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-f), other.start.T(f))
909}
910
911func (e element) sizeEqDouble(other element) casso.Constraint {
912	return casso.NewConstraint(casso.EQ, 0, e.end.T(1), e.start.T(-1), other.end.T(-2), other.start.T(2))
913}
914
915func combinations(n, k int) [][]int {
916	combs := binomial(n, k)
917	data := make([][]int, combs)
918	if len(data) == 0 {
919		return nil
920	}
921
922	data[0] = make([]int, k)
923	for i := range data[0] {
924		data[0][i] = i
925	}
926
927	for i := 1; i < combs; i++ {
928		next := make([]int, k)
929		copy(next, data[i-1])
930		nextCombination(next, n, k)
931		data[i] = next
932	}
933
934	return data
935}
936
937func nextCombination(s []int, n, k int) {
938	for j := k - 1; j >= 0; j-- {
939		if s[j] == n+j-k {
940			continue
941		}
942
943		s[j]++
944
945		for l := j + 1; l < k; l++ {
946			s[l] = s[j] + l - j
947		}
948
949		break
950	}
951}
952
953func binomial(n, k int) int {
954	if n < 0 || k < 0 {
955		panic("layout: binomial: negative input")
956	}
957
958	if n < k {
959		return 0
960	}
961
962	// (n,k) = (n, n-k)
963	if k > n/2 {
964		k = n - k
965	}
966
967	b := 1
968	for i := 1; i <= k; i++ {
969		b = (n - k + i) * b / i
970	}
971
972	return b
973}