1package layout
2
3import (
4 "errors"
5 "math"
6 "sort"
7
8 "github.com/bnema/vev/internal/domain"
9)
10
11var (
12 ErrNotFound = errors.New("pane not found")
13 ErrTooSmall = errors.New("layout too small")
14 ErrNoPane = errors.New("no pane in direction")
15 ErrNotInSplit = errors.New("pane is not in a split")
16 ErrNotToggleable = errors.New("layout node is not toggleable")
17)
18
19// ResizeFocus changes the focused pane subtree's share in the nearest split on
20// axis. It commits only after the candidate layout solves successfully.
21func (t *Tree) ResizeFocus(axis Axis, delta int, area domain.Rect) error {
22 candidate := t.clone()
23 if candidate == nil || candidate.Root == nil {
24 return ErrNotInSplit
25 }
26 dir, ok := splitDirForAxis(axis)
27 if !ok || !hasMatchingSplit(candidate.Root, candidate.Focus, dir) {
28 return ErrNotInSplit
29 }
30 if _, solved := Solve(candidate.Root, area); !solved {
31 return ErrTooSmall
32 }
33
34 splitNode, splitArea, targetIndex, found := nearestMatchingSplit(candidate.Root, candidate.Focus, dir, area)
35 if !found {
36 return ErrTooSmall
37 }
38 rects, solved := splitChildRects(splitNode, splitArea)
39 if !solved {
40 return ErrTooSmall
41 }
42 extents := make([]int, len(rects))
43 minimums := make([]int, len(rects))
44 for i, rect := range rects {
45 extents[i] = rect.Width
46 if dir == Vertical {
47 extents[i] = rect.Height
48 }
49 minimum, valid := minimumExtent(splitNode.Children[i], dir)
50 if !valid {
51 return ErrTooSmall
52 }
53 minimums[i] = minimum
54 splitNode.Children[i].Weight = float64(extents[i])
55 }
56
57 requested := delta
58 if requested < 0 {
59 if requested == math.MinInt {
60 requested = math.MaxInt
61 } else {
62 requested = -requested
63 }
64 }
65 if requested == 0 {
66 return ErrTooSmall
67 }
68
69 neighbor := -1
70 transfer := 0
71 if delta > 0 {
72 for _, index := range preferredNeighbors(targetIndex, len(extents)) {
73 available := extents[index] - minimums[index]
74 if available <= 0 {
75 continue
76 }
77 neighbor = index
78 transfer = min(requested, available)
79 break
80 }
81 } else {
82 neighbors := preferredNeighbors(targetIndex, len(extents))
83 available := extents[targetIndex] - minimums[targetIndex]
84 if len(neighbors) > 0 && available > 0 {
85 neighbor = neighbors[0]
86 transfer = min(requested, available)
87 }
88 }
89 if neighbor < 0 || transfer == 0 {
90 return ErrTooSmall
91 }
92
93 if delta > 0 {
94 extents[targetIndex] += transfer
95 extents[neighbor] -= transfer
96 } else {
97 extents[targetIndex] -= transfer
98 extents[neighbor] += transfer
99 }
100 splitNode.Children[targetIndex].Weight = float64(extents[targetIndex])
101 splitNode.Children[neighbor].Weight = float64(extents[neighbor])
102 if _, solved := Solve(candidate.Root, area); !solved {
103 return ErrTooSmall
104 }
105 *t = *candidate
106 return nil
107}
108
109// Equalize resets every split's direct child shares to their defaults. The
110// candidate must remain solvable before it is committed.
111func (t *Tree) Equalize(area domain.Rect) error {
112 candidate := t.clone()
113 if candidate == nil || candidate.Root == nil {
114 return ErrTooSmall
115 }
116 clearSplitChildWeights(candidate.Root)
117 if _, ok := Solve(candidate.Root, area); !ok {
118 return ErrTooSmall
119 }
120 *t = *candidate
121 return nil
122}
123
124// CanResize reports whether the focused pane is contained by any split.
125func (t *Tree) CanResize() bool {
126 if t == nil || t.Root == nil {
127 return false
128 }
129 return hasMatchingSplit(t.Root, t.Focus, Horizontal) || hasMatchingSplit(t.Root, t.Focus, Vertical)
130}
131
132func splitDirForAxis(axis Axis) (SplitDir, bool) {
133 switch axis {
134 case Width:
135 return Horizontal, true
136 case Height:
137 return Vertical, true
138 default:
139 return Horizontal, false
140 }
141}
142
143func hasMatchingSplit(n *Node, focus PaneID, dir SplitDir) bool {
144 if n == nil || !containsLeaf(n, focus) {
145 return false
146 }
147 for _, child := range n.Children {
148 if containsLeaf(child, focus) && hasMatchingSplit(child, focus, dir) {
149 return true
150 }
151 }
152 return n.Kind == Split && n.Dir == dir
153}
154
155func nearestMatchingSplit(n *Node, focus PaneID, dir SplitDir, area domain.Rect) (*Node, domain.Rect, int, bool) {
156 if n == nil || !containsLeaf(n, focus) {
157 return nil, domain.Rect{}, 0, false
158 }
159 childAreas := nodeChildAreas(n, area)
160 for i, child := range n.Children {
161 if !containsLeaf(child, focus) {
162 continue
163 }
164 childArea := area
165 if i < len(childAreas) {
166 childArea = childAreas[i]
167 }
168 if match, matchArea, index, ok := nearestMatchingSplit(child, focus, dir, childArea); ok {
169 return match, matchArea, index, true
170 }
171 if n.Kind == Split && n.Dir == dir {
172 return n, area, i, true
173 }
174 return nil, domain.Rect{}, 0, false
175 }
176 return nil, domain.Rect{}, 0, false
177}
178
179func preferredNeighbors(target, count int) []int {
180 neighbors := make([]int, 0, 2)
181 if target+1 < count {
182 neighbors = append(neighbors, target+1)
183 }
184 if target > 0 {
185 neighbors = append(neighbors, target-1)
186 }
187 return neighbors
188}
189
190func clearSplitChildWeights(n *Node) {
191 if n == nil {
192 return
193 }
194 if n.Kind == Split {
195 clearChildWeights(n)
196 }
197 for _, child := range n.Children {
198 clearSplitChildWeights(child)
199 }
200}
201
202// Split adds newID next to target. The after flag controls whether the new pane
203// is placed after target on the chosen axis.
204func (t *Tree) Split(target PaneID, dir Direction, after bool, newID PaneID, area domain.Rect) error {
205 candidate := t.clone()
206 if candidate == nil || candidate.Root == nil || !insertSplit(candidate.Root, target, axisFor(dir), after, newID, area) {
207 return ErrNotFound
208 }
209 candidate.Focus = newID
210 if _, ok := Solve(candidate.Root, area); !ok {
211 return ErrTooSmall
212 }
213 *t = *candidate
214 return nil
215}
216
217func insertSplit(n *Node, target PaneID, axis SplitDir, after bool, newID PaneID, area domain.Rect) bool {
218 if n == nil {
219 return false
220 }
221 childAreas := nodeChildAreas(n, area)
222 for i, child := range n.Children {
223 if child.Kind == Leaf && child.Leaf == target {
224 newLeaf := NewLeaf(newID)
225 if n.Kind == Split && n.Dir == axis {
226 normalizeChildWeightsFromArea(n, area)
227 targetExtent := childAreas[i].Width
228 if axis == Vertical {
229 targetExtent = childAreas[i].Height
230 }
231 firstExtent := (targetExtent - 1) / 2
232 child.Weight = float64(firstExtent)
233 newLeaf.Weight = float64(targetExtent - 1 - firstExtent)
234 at := i
235 if after {
236 at++
237 }
238 n.Children = append(n.Children, nil)
239 copy(n.Children[at+1:], n.Children[at:])
240 n.Children[at] = newLeaf
241 return true
242 }
243 if n.Kind == Stack {
244 weight := n.Weight
245 stack := n.clone()
246 stack.Weight = 0
247 clearChildWeights(stack)
248 children := []*Node{stack, newLeaf}
249 if !after {
250 children = []*Node{newLeaf, stack}
251 }
252 *n = Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
253 return true
254 }
255 weight := child.Weight
256 child.Weight = 0
257 children := []*Node{child, newLeaf}
258 if !after {
259 children = []*Node{newLeaf, child}
260 }
261 n.Children[i] = &Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
262 return true
263 }
264 childArea := area
265 if i < len(childAreas) {
266 childArea = childAreas[i]
267 }
268 if insertSplit(child, target, axis, after, newID, childArea) {
269 return true
270 }
271 }
272 if n.Kind == Leaf && n.Leaf == target {
273 weight := n.Weight
274 existing := n.clone()
275 existing.Weight = 0
276 children := []*Node{existing, NewLeaf(newID)}
277 if !after {
278 children = []*Node{NewLeaf(newID), existing}
279 }
280 *n = Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
281 return true
282 }
283 return false
284}
285
286func nodeChildAreas(n *Node, area domain.Rect) []domain.Rect {
287 if n.Kind == Split {
288 rects, ok := splitChildRects(n, area)
289 if ok {
290 return rects
291 }
292 }
293 areas := make([]domain.Rect, len(n.Children))
294 for i := range areas {
295 areas[i] = area
296 }
297 return areas
298}
299
300func normalizeChildWeightsFromArea(n *Node, area domain.Rect) {
301 rects, ok := splitChildRects(n, area)
302 if !ok || len(rects) == 0 {
303 return
304 }
305 for i, rect := range rects {
306 extent := rect.Width
307 if n.Dir == Vertical {
308 extent = rect.Height
309 }
310 n.Children[i].Weight = float64(extent)
311 }
312}
313
314func clearChildWeights(n *Node) {
315 for _, child := range n.Children {
316 child.Weight = 0
317 }
318}
319
320// StackNew puts newID in target's stack, creating one if target is not stacked.
321func (t *Tree) StackNew(target, newID PaneID, area domain.Rect) error {
322 candidate := t.clone()
323 if candidate == nil || !stackNew(candidate.Root, target, newID) {
324 return ErrNotFound
325 }
326 candidate.Focus = newID
327 if _, ok := Solve(candidate.Root, area); !ok {
328 return ErrTooSmall
329 }
330 *t = *candidate
331 return nil
332}
333
334func stackNew(n *Node, target, newID PaneID) bool {
335 if n == nil {
336 return false
337 }
338 if n.Kind == Stack {
339 for _, child := range n.Children {
340 if child.Kind == Leaf && child.Leaf == target {
341 n.Children = append(n.Children, NewLeaf(newID))
342 n.Expanded = newID
343 clearChildWeights(n)
344 return true
345 }
346 }
347 }
348 for i, child := range n.Children {
349 if child.Kind == Leaf && child.Leaf == target {
350 weight := child.Weight
351 child.Weight = 0
352 n.Children[i] = &Node{Kind: Stack, Children: []*Node{child, NewLeaf(newID)}, Expanded: newID, Weight: weight}
353 return true
354 }
355 if stackNew(child, target, newID) {
356 return true
357 }
358 }
359 if n.Kind == Leaf && n.Leaf == target {
360 weight := n.Weight
361 *n = Node{Kind: Stack, Children: []*Node{NewLeaf(target), NewLeaf(newID)}, Expanded: newID, Weight: weight}
362 return true
363 }
364 return false
365}
366
367// ToggleStack converts a stack back to a vertical split. For an unstacked leaf in
368// a two-child split, it converts that split to a stack.
369func (t *Tree) ToggleStack(target PaneID, area domain.Rect) error {
370 candidate := t.clone()
371 result := toggleStack(candidate.Root, target)
372 switch result {
373 case toggleNotFound:
374 return ErrNotFound
375 case toggleNotToggleable:
376 return ErrNotToggleable
377 }
378 if _, ok := Solve(candidate.Root, area); !ok {
379 return ErrTooSmall
380 }
381 *t = *candidate
382 return nil
383}
384
385type toggleResult int
386
387const (
388 toggleNotFound toggleResult = iota
389 toggleToggled
390 toggleNotToggleable
391)
392
393func toggleStack(n *Node, target PaneID) toggleResult {
394 if n == nil {
395 return toggleNotFound
396 }
397 for _, child := range n.Children {
398 if child.Kind == Leaf || !containsLeaf(child, target) {
399 continue
400 }
401 result := toggleStack(child, target)
402 if result != toggleNotFound {
403 return result
404 }
405 }
406 if !containsLeaf(n, target) {
407 return toggleNotFound
408 }
409 if n.Kind == Stack {
410 if stackDirectLeafIndex(n, target) < 0 {
411 return toggleNotToggleable
412 }
413 n.Kind = Split
414 n.Dir = Vertical
415 n.Expanded = ""
416 clearChildWeights(n)
417 return toggleToggled
418 }
419 if n.Kind == Split && len(n.Children) == 2 && splitDirectLeavesContain(n, target) {
420 n.Kind = Stack
421 n.Expanded = target
422 clearChildWeights(n)
423 return toggleToggled
424 }
425 return toggleNotToggleable
426}
427
428func stackDirectLeafIndex(n *Node, target PaneID) int {
429 for i, child := range n.Children {
430 if child.Kind == Leaf && child.Leaf == target {
431 return i
432 }
433 }
434 return -1
435}
436
437func splitDirectLeavesContain(n *Node, target PaneID) bool {
438 for _, child := range n.Children {
439 if child.Kind != Leaf {
440 return false
441 }
442 }
443 return n.Children[0].Leaf == target || n.Children[1].Leaf == target
444}
445
446// Close removes target and dissolves single-child containers.
447func (t *Tree) Close(target PaneID) error {
448 candidate := t.clone()
449 if candidate == nil || candidate.Root == nil || !containsLeaf(candidate.Root, target) {
450 return ErrNotFound
451 }
452 if candidate.Root.Kind == Leaf && candidate.Root.Leaf == target {
453 candidate.Root = nil
454 candidate.Focus = ""
455 *t = *candidate
456 return nil
457 }
458 refocus := closeRefocusCandidate(candidate.Root, target)
459 root, removed := closeNode(candidate.Root, target)
460 if !removed {
461 return ErrNotFound
462 }
463 candidate.Root = root
464 if candidate.Focus == target {
465 if refocus != "" && containsLeaf(candidate.Root, refocus) {
466 candidate.Focus = refocus
467 } else {
468 ids := leafIDs(candidate.Root)
469 if len(ids) > 0 {
470 candidate.Focus = ids[0]
471 } else {
472 candidate.Focus = ""
473 }
474 }
475 setExpanded(candidate.Root, candidate.Focus)
476 }
477 *t = *candidate
478 return nil
479}
480
481func closeNode(n *Node, target PaneID) (*Node, bool) {
482 if n.Kind == Leaf {
483 if n.Leaf == target {
484 return nil, true
485 }
486 return n, false
487 }
488 removed := false
489 children := n.Children[:0]
490 for _, child := range n.Children {
491 newChild, didRemove := closeNode(child, target)
492 removed = removed || didRemove
493 if newChild != nil {
494 children = append(children, newChild)
495 }
496 }
497 n.Children = children
498 if !removed {
499 return n, false
500 }
501 if n.Kind == Stack && n.Expanded == target && len(n.Children) > 0 {
502 n.Expanded = firstLeaf(n.Children[0])
503 }
504 if (n.Kind == Stack || n.Kind == Split) && len(n.Children) == 1 {
505 promoted := n.Children[0]
506 promoted.Weight = n.Weight
507 return promoted, true
508 }
509 return n, true
510}
511
512func closeRefocusCandidate(n *Node, target PaneID) PaneID {
513 if n == nil || n.Kind == Leaf {
514 return ""
515 }
516 for i, child := range n.Children {
517 if !containsLeaf(child, target) {
518 continue
519 }
520 if child.Kind == Leaf && child.Leaf == target {
521 if i > 0 {
522 return lastLeaf(n.Children[i-1])
523 }
524 if i+1 < len(n.Children) {
525 return firstLeaf(n.Children[i+1])
526 }
527 return ""
528 }
529 if id := closeRefocusCandidate(child, target); id != "" {
530 return id
531 }
532 if i > 0 {
533 return lastLeaf(n.Children[i-1])
534 }
535 if i+1 < len(n.Children) {
536 return firstLeaf(n.Children[i+1])
537 }
538 return ""
539 }
540 return ""
541}
542
543// ContainsLeaf reports whether n contains a leaf with id.
544func ContainsLeaf(n *Node, id PaneID) bool { return containsLeaf(n, id) }
545
546func containsLeaf(n *Node, id PaneID) bool {
547 if n == nil {
548 return false
549 }
550 if n.Kind == Leaf {
551 return n.Leaf == id
552 }
553 for _, child := range n.Children {
554 if containsLeaf(child, id) {
555 return true
556 }
557 }
558 return false
559}
560
561func firstLeaf(n *Node) PaneID {
562 if n.Kind == Leaf {
563 return n.Leaf
564 }
565 for _, child := range n.Children {
566 if id := firstLeaf(child); id != "" {
567 return id
568 }
569 }
570 return ""
571}
572
573func lastLeaf(n *Node) PaneID {
574 if n.Kind == Leaf {
575 return n.Leaf
576 }
577 for i := len(n.Children) - 1; i >= 0; i-- {
578 if id := lastLeaf(n.Children[i]); id != "" {
579 return id
580 }
581 }
582 return ""
583}
584
585func setExpanded(n *Node, id PaneID) bool {
586 if n == nil {
587 return false
588 }
589 if n.Kind == Stack && stackDirectLeafIndex(n, id) >= 0 {
590 n.Expanded = id
591 return true
592 }
593 for _, child := range n.Children {
594 if setExpanded(child, id) {
595 return true
596 }
597 }
598 return false
599}
600
601// LeafIDs returns the leaf ids under n in layout order.
602func LeafIDs(n *Node) []PaneID { return leafIDs(n) }
603
604func leafIDs(n *Node) []PaneID {
605 if n == nil {
606 return nil
607 }
608 if n.Kind == Leaf {
609 return []PaneID{n.Leaf}
610 }
611 var ids []PaneID
612 for _, child := range n.Children {
613 ids = append(ids, leafIDs(child)...)
614 }
615 return ids
616}
617
618// FocusSpan returns the solved focus rectangle of the focused pane.
619func (t *Tree) FocusSpan(area domain.Rect) (domain.Rect, error) {
620 if t == nil || t.Root == nil {
621 return domain.Rect{}, ErrNotFound
622 }
623 placements, ok := Solve(t.Root, area)
624 if !ok {
625 return domain.Rect{}, ErrTooSmall
626 }
627 for _, placement := range placements {
628 if placement.ID == t.Focus {
629 return focusRect(placement), nil
630 }
631 }
632 return domain.Rect{}, ErrNotFound
633}
634
635// EntryPane returns the pane on the facing edge that best preserves span on
636// the perpendicular axis. It does not mutate the tree.
637func (t *Tree) EntryPane(dir Direction, span, area domain.Rect) (PaneID, error) {
638 if t == nil || t.Root == nil {
639 return "", ErrNotFound
640 }
641 placements, ok := Solve(t.Root, area)
642 if !ok {
643 return "", ErrTooSmall
644 }
645
646 outside := span
647 switch dir {
648 case Left:
649 outside.X = area.X + area.Width
650 case Right:
651 outside.X = area.X - span.Width
652 case Up:
653 outside.Y = area.Y + area.Height
654 case Down:
655 outside.Y = area.Y - span.Height
656 default:
657 return "", ErrNoPane
658 }
659
660 return selectFacingCandidate(placements, outside, dir, facingGapFirst)
661}
662
663// FocusEnter focuses the best pane on the facing edge and expands it when it
664// is a collapsed stack member.
665func (t *Tree) FocusEnter(dir Direction, span, area domain.Rect) error {
666 id, err := t.EntryPane(dir, span, area)
667 if err != nil {
668 return err
669 }
670 t.Focus = id
671 setExpanded(t.Root, id)
672 return nil
673}
674
675// FocusDir moves focus in dir. Vertical movement within a stack walks stack
676// members before falling back to geometric focus.
677func (t *Tree) FocusDir(dir Direction, area domain.Rect) error {
678 if t.Root == nil {
679 return ErrNotFound
680 }
681 if (dir == Up || dir == Down) && focusStackLocal(t.Root, t.Focus, dir, &t.Focus) {
682 return nil
683 }
684 placements, ok := Solve(t.Root, area)
685 if !ok {
686 return ErrTooSmall
687 }
688 currentIdx := -1
689 for i, p := range placements {
690 if p.ID == t.Focus {
691 currentIdx = i
692 break
693 }
694 }
695 if currentIdx < 0 {
696 return ErrNotFound
697 }
698 current := focusRect(placements[currentIdx])
699 candidate, err := selectFacingCandidate(placements, current, dir, facingOverlapFirst)
700 if err != nil {
701 return err
702 }
703 t.Focus = candidate
704 setExpanded(t.Root, t.Focus)
705 return nil
706}
707
708type facingMetricPriority uint8
709
710const (
711 facingGapFirst facingMetricPriority = iota
712 facingOverlapFirst
713)
714
715type facingCandidate struct {
716 id PaneID
717 gap int
718 overlap int
719}
720
721func selectFacingCandidate(placements []Placement, from domain.Rect, dir Direction, priority facingMetricPriority) (PaneID, error) {
722 candidates := make([]facingCandidate, 0, len(placements))
723 for _, placement := range placements {
724 if facing, gap, perpendicularOverlap := sideGapOverlap(from, focusRect(placement), dir); facing {
725 candidates = append(candidates, facingCandidate{id: placement.ID, gap: gap, overlap: perpendicularOverlap})
726 }
727 }
728 if len(candidates) == 0 {
729 return "", ErrNoPane
730 }
731 sort.Slice(candidates, func(i, j int) bool {
732 if priority == facingGapFirst {
733 if candidates[i].gap != candidates[j].gap {
734 return candidates[i].gap < candidates[j].gap
735 }
736 if candidates[i].overlap != candidates[j].overlap {
737 return candidates[i].overlap > candidates[j].overlap
738 }
739 } else {
740 if candidates[i].overlap != candidates[j].overlap {
741 return candidates[i].overlap > candidates[j].overlap
742 }
743 if candidates[i].gap != candidates[j].gap {
744 return candidates[i].gap < candidates[j].gap
745 }
746 }
747 return candidates[i].id < candidates[j].id
748 })
749 return candidates[0].id, nil
750}
751
752func focusStackLocal(n *Node, id PaneID, dir Direction, focus *PaneID) bool {
753 if n.Kind == Stack {
754 idx := stackDirectLeafIndex(n, id)
755 if idx >= 0 {
756 if next := adjacentStackLeaf(n, idx, dir); next != "" {
757 n.Expanded = next
758 *focus = next
759 return true
760 }
761 return false
762 }
763 }
764 for _, child := range n.Children {
765 if focusStackLocal(child, id, dir, focus) {
766 return true
767 }
768 }
769 return false
770}
771
772func adjacentStackLeaf(n *Node, idx int, dir Direction) PaneID {
773 step := 0
774 switch dir {
775 case Up:
776 step = -1
777 case Down:
778 step = 1
779 default:
780 return ""
781 }
782 for i := idx + step; i >= 0 && i < len(n.Children); i += step {
783 if n.Children[i].Kind == Leaf {
784 return n.Children[i].Leaf
785 }
786 }
787 return ""
788}
789
790func focusRect(p Placement) domain.Rect {
791 if p.TitleBar.Height == 0 {
792 return p.Content
793 }
794 if p.Content.Height == 0 {
795 return p.TitleBar
796 }
797 y := p.TitleBar.Y
798 height := p.Content.Y + p.Content.Height - y
799 return domain.Rect{X: p.TitleBar.X, Y: y, Width: p.TitleBar.Width, Height: height}
800}
801
802func sideGapOverlap(a, b domain.Rect, dir Direction) (bool, int, int) {
803 ar, ab := a.X+a.Width, a.Y+a.Height
804 br, bb := b.X+b.Width, b.Y+b.Height
805 switch dir {
806 case Left:
807 if br > a.X {
808 return false, 0, 0
809 }
810 return true, a.X - br, overlap(a.Y, ab, b.Y, bb)
811 case Right:
812 if b.X < ar {
813 return false, 0, 0
814 }
815 return true, b.X - ar, overlap(a.Y, ab, b.Y, bb)
816 case Up:
817 if bb > a.Y {
818 return false, 0, 0
819 }
820 return true, a.Y - bb, overlap(a.X, ar, b.X, br)
821 case Down:
822 if b.Y < ab {
823 return false, 0, 0
824 }
825 return true, b.Y - ab, overlap(a.X, ar, b.X, br)
826 default:
827 return false, 0, 0
828 }
829}
830
831func overlap(a0, a1, b0, b1 int) int {
832 lo := max(a0, b0)
833 hi := min(a1, b1)
834 if hi <= lo {
835 return 0
836 }
837 return hi - lo
838}