1package daemon
2
3import (
4 "errors"
5 "fmt"
6 "math"
7
8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/domain/layout"
10)
11
12// movePaneCandidate contains only unpublished topology and identity changes.
13// The caller must revalidate the live tabs before committing these values.
14type movePaneCandidate struct {
15 pane *pane
16
17 sourceID layout.PaneID
18 destinationID layout.PaneID
19 removeSourceTab bool
20
21 sourceTree *layout.Tree
22 sourcePlacements []layout.Placement
23 destinationTree *layout.Tree
24 destinationPlacements []layout.Placement
25 destinationNextPaneID int
26 sourceGeneration uint64
27 destinationGeneration uint64
28}
29
30// prepareMovePaneCandidate builds both affected layouts without publishing any
31// tab or pane state. The caller is responsible for holding and later
32// revalidating the source and destination tab state.
33func prepareMovePaneCandidate(source, destination *tab, moved *pane) (*movePaneCandidate, error) {
34 if source == nil || destination == nil || source == destination || moved == nil ||
35 source.tree == nil || source.tree.Root == nil || source.panes[moved.id] != moved ||
36 !layout.ContainsLeaf(source.tree.Root, moved.id) || destination.tree == nil || destination.tree.Root == nil ||
37 destination.panes[destination.tree.Focus] == nil || !layout.ContainsLeaf(destination.tree.Root, destination.tree.Focus) {
38 return nil, errMovePaneInvalid
39 }
40
41 candidate := &movePaneCandidate{
42 pane: moved,
43 sourceID: moved.id,
44 sourceGeneration: source.layoutGeneration,
45 destinationGeneration: destination.layoutGeneration,
46 }
47
48 sourceLeaves := layout.LeafIDs(source.tree.Root)
49 if len(sourceLeaves) == 1 {
50 switch source.floating.state {
51 case floatingWarming:
52 return nil, errMoveFloatingWarming
53 case floatingHidden, floatingVisible:
54 return nil, errMoveFinalSourceFloating
55 }
56 candidate.removeSourceTab = true
57 } else {
58 candidate.sourceTree = source.tree.Clone()
59 if err := candidate.sourceTree.Close(moved.id); err != nil {
60 return nil, errMovePaneInvalid
61 }
62 var ok bool
63 candidate.sourcePlacements, ok = layout.Solve(candidate.sourceTree.Root, tabArea(source))
64 if !ok || candidate.sourceTree.Focus == "" || !layout.ContainsLeaf(candidate.sourceTree.Root, candidate.sourceTree.Focus) {
65 return nil, errMoveTooSmall
66 }
67 }
68
69 var err error
70 candidate.destinationID, candidate.destinationNextPaneID, err = allocateMovePaneID(destination, moved.id)
71 if err != nil {
72 return nil, err
73 }
74 candidate.destinationTree = destination.tree.Clone()
75 area := tabArea(destination)
76 if err := candidate.destinationTree.Split(destination.tree.Focus, layout.Right, true, candidate.destinationID, area); err != nil {
77 if errors.Is(err, layout.ErrTooSmall) {
78 return nil, errMoveTooSmall
79 }
80 return nil, errMovePaneInvalid
81 }
82 var ok bool
83 candidate.destinationPlacements, ok = layout.Solve(candidate.destinationTree.Root, area)
84 if !ok {
85 return nil, errMoveTooSmall
86 }
87 return candidate, nil
88}
89
90func allocateMovePaneID(destination *tab, sourceID layout.PaneID) (layout.PaneID, int, error) {
91 next := max(destination.nextPaneID, 1)
92 for {
93 id := layout.PaneID(fmt.Sprintf("pane-%d", next))
94 _, mapped := destination.panes[id]
95 available := id != sourceID && !mapped && !layout.ContainsLeaf(destination.tree.Root, id)
96 if available {
97 if next == math.MaxInt {
98 return id, next, nil
99 }
100 return id, next + 1, nil
101 }
102 if next == math.MaxInt {
103 return "", 0, errMovePaneIDExhausted
104 }
105 next++
106 }
107}
108
109func tabArea(tb *tab) domain.Rect {
110 return domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}
111}