Merge pull request #202 from bnema/fix/nested-split-geometry
fix(layout): halve the targeted pane on repeated splits
3 files · +66 -3
M internal/usecase/layout/layout_test.go +56 -0
| @@ -43,6 +43,62 @@ func TestSolveSplitGeometry(t *testing.T) { | |||
| 43 | 43 | } | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | + | func TestRepeatedSplitHalvesFocusedPane(t *testing.T) { | |
| 47 | + | t.Parallel() | |
| 48 | + | ||
| 49 | + | tests := []struct { | |
| 50 | + | name string | |
| 51 | + | dir Direction | |
| 52 | + | area domain.Rect | |
| 53 | + | first map[PaneID]domain.Rect | |
| 54 | + | second map[PaneID]domain.Rect | |
| 55 | + | }{ | |
| 56 | + | { | |
| 57 | + | name: "horizontal", | |
| 58 | + | dir: Right, | |
| 59 | + | area: domain.Rect{Width: 123, Height: 5}, | |
| 60 | + | first: map[PaneID]domain.Rect{ | |
| 61 | + | "a": {Width: 61, Height: 5}, | |
| 62 | + | "b": {X: 62, Width: 61, Height: 5}, | |
| 63 | + | }, | |
| 64 | + | second: map[PaneID]domain.Rect{ | |
| 65 | + | "a": {Width: 61, Height: 5}, | |
| 66 | + | "b": {X: 62, Width: 30, Height: 5}, | |
| 67 | + | "c": {X: 93, Width: 30, Height: 5}, | |
| 68 | + | }, | |
| 69 | + | }, | |
| 70 | + | { | |
| 71 | + | name: "vertical", | |
| 72 | + | dir: Down, | |
| 73 | + | area: domain.Rect{Width: 40, Height: 15}, | |
| 74 | + | first: map[PaneID]domain.Rect{ | |
| 75 | + | "a": {Width: 40, Height: 7}, | |
| 76 | + | "b": {Y: 8, Width: 40, Height: 7}, | |
| 77 | + | }, | |
| 78 | + | second: map[PaneID]domain.Rect{ | |
| 79 | + | "a": {Width: 40, Height: 7}, | |
| 80 | + | "b": {Y: 8, Width: 40, Height: 3}, | |
| 81 | + | "c": {Y: 12, Width: 40, Height: 3}, | |
| 82 | + | }, | |
| 83 | + | }, | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | for _, tt := range tests { | |
| 87 | + | t.Run(tt.name, func(t *testing.T) { | |
| 88 | + | tr := NewTree("a") | |
| 89 | + | require.NoError(t, tr.Split("a", tt.dir, true, "b", tt.area)) | |
| 90 | + | first, ok := Solve(tr.Root, tt.area) | |
| 91 | + | require.True(t, ok) | |
| 92 | + | require.Equal(t, tt.first, contents(first)) | |
| 93 | + | ||
| 94 | + | require.NoError(t, tr.Split("b", tt.dir, true, "c", tt.area)) | |
| 95 | + | second, ok := Solve(tr.Root, tt.area) | |
| 96 | + | require.True(t, ok) | |
| 97 | + | require.Equal(t, tt.second, contents(second)) | |
| 98 | + | }) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 46 | 102 | func TestSolveNestedTrees(t *testing.T) { | |
| 47 | 103 | t.Parallel() | |
| 48 | 104 | root := &Node{Kind: Split, Dir: Horizontal, Children: []*Node{ | |
| @@ -43,6 +43,62 @@ func TestSolveSplitGeometry(t *testing.T) { | |||||
| 43 | } | 43 | } | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | + | func TestRepeatedSplitHalvesFocusedPane(t *testing.T) { | |||
| 47 | + | t.Parallel() | |||
| 48 | + | ||||
| 49 | + | tests := []struct { | |||
| 50 | + | name string | |||
| 51 | + | dir Direction | |||
| 52 | + | area domain.Rect | |||
| 53 | + | first map[PaneID]domain.Rect | |||
| 54 | + | second map[PaneID]domain.Rect | |||
| 55 | + | }{ | |||
| 56 | + | { | |||
| 57 | + | name: "horizontal", | |||
| 58 | + | dir: Right, | |||
| 59 | + | area: domain.Rect{Width: 123, Height: 5}, | |||
| 60 | + | first: map[PaneID]domain.Rect{ | |||
| 61 | + | "a": {Width: 61, Height: 5}, | |||
| 62 | + | "b": {X: 62, Width: 61, Height: 5}, | |||
| 63 | + | }, | |||
| 64 | + | second: map[PaneID]domain.Rect{ | |||
| 65 | + | "a": {Width: 61, Height: 5}, | |||
| 66 | + | "b": {X: 62, Width: 30, Height: 5}, | |||
| 67 | + | "c": {X: 93, Width: 30, Height: 5}, | |||
| 68 | + | }, | |||
| 69 | + | }, | |||
| 70 | + | { | |||
| 71 | + | name: "vertical", | |||
| 72 | + | dir: Down, | |||
| 73 | + | area: domain.Rect{Width: 40, Height: 15}, | |||
| 74 | + | first: map[PaneID]domain.Rect{ | |||
| 75 | + | "a": {Width: 40, Height: 7}, | |||
| 76 | + | "b": {Y: 8, Width: 40, Height: 7}, | |||
| 77 | + | }, | |||
| 78 | + | second: map[PaneID]domain.Rect{ | |||
| 79 | + | "a": {Width: 40, Height: 7}, | |||
| 80 | + | "b": {Y: 8, Width: 40, Height: 3}, | |||
| 81 | + | "c": {Y: 12, Width: 40, Height: 3}, | |||
| 82 | + | }, | |||
| 83 | + | }, | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | for _, tt := range tests { | |||
| 87 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 88 | + | tr := NewTree("a") | |||
| 89 | + | require.NoError(t, tr.Split("a", tt.dir, true, "b", tt.area)) | |||
| 90 | + | first, ok := Solve(tr.Root, tt.area) | |||
| 91 | + | require.True(t, ok) | |||
| 92 | + | require.Equal(t, tt.first, contents(first)) | |||
| 93 | + | ||||
| 94 | + | require.NoError(t, tr.Split("b", tt.dir, true, "c", tt.area)) | |||
| 95 | + | second, ok := Solve(tr.Root, tt.area) | |||
| 96 | + | require.True(t, ok) | |||
| 97 | + | require.Equal(t, tt.second, contents(second)) | |||
| 98 | + | }) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 46 | func TestSolveNestedTrees(t *testing.T) { | 102 | func TestSolveNestedTrees(t *testing.T) { | ||
| 47 | t.Parallel() | 103 | t.Parallel() | ||
| 48 | root := &Node{Kind: Split, Dir: Horizontal, Children: []*Node{ | 104 | root := &Node{Kind: Split, Dir: Horizontal, Children: []*Node{ | ||
M internal/usecase/layout/ops.go +7 -0
| @@ -224,6 +224,13 @@ func insertSplit(n *Node, target PaneID, axis SplitDir, after bool, newID PaneID | |||
| 224 | 224 | newLeaf := NewLeaf(newID) | |
| 225 | 225 | if n.Kind == Split && n.Dir == axis { | |
| 226 | 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) | |
| 227 | 234 | at := i | |
| 228 | 235 | if after { | |
| 229 | 236 | at++ | |
| @@ -224,6 +224,13 @@ func insertSplit(n *Node, target PaneID, axis SplitDir, after bool, newID PaneID | |||||
| 224 | newLeaf := NewLeaf(newID) | 224 | newLeaf := NewLeaf(newID) | ||
| 225 | if n.Kind == Split && n.Dir == axis { | 225 | if n.Kind == Split && n.Dir == axis { | ||
| 226 | normalizeChildWeightsFromArea(n, area) | 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) | |||
| 227 | at := i | 234 | at := i | ||
| 228 | if after { | 235 | if after { | ||
| 229 | at++ | 236 | at++ | ||
M internal/usecase/layout/weights_test.go +3 -3
| @@ -159,7 +159,7 @@ func TestWeightedSplitRecursiveMinima(t *testing.T) { | |||
| 159 | 159 | } | |
| 160 | 160 | } | |
| 161 | 161 | ||
| 162 | - | func TestWeightSameAxisSplitNormalizesSolvedExtentsBeforeInsertion(t *testing.T) { | |
| 162 | + | func TestWeightSameAxisSplitHalvesTargetSolvedExtent(t *testing.T) { | |
| 163 | 163 | t.Parallel() | |
| 164 | 164 | ||
| 165 | 165 | tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)), Focus: "a"} | |
| @@ -167,8 +167,8 @@ func TestWeightSameAxisSplitNormalizesSolvedExtentsBeforeInsertion(t *testing.T) | |||
| 167 | 167 | require.NoError(t, tr.Split("b", Right, true, "c", area)) | |
| 168 | 168 | ||
| 169 | 169 | require.Equal(t, 54.0, tr.Root.Children[0].Weight) | |
| 170 | - | require.Equal(t, 27.0, tr.Root.Children[1].Weight) | |
| 171 | - | require.Zero(t, tr.Root.Children[2].Weight) | |
| 170 | + | require.Equal(t, 13.0, tr.Root.Children[1].Weight) | |
| 171 | + | require.Equal(t, 13.0, tr.Root.Children[2].Weight) | |
| 172 | 172 | placements, ok := Solve(tr.Root, area) | |
| 173 | 173 | require.True(t, ok) | |
| 174 | 174 | require.Equal(t, map[PaneID]int{"a": 40, "b": 20, "c": 20}, placementWidths(placements)) | |
| @@ -159,7 +159,7 @@ func TestWeightedSplitRecursiveMinima(t *testing.T) { | |||||
| 159 | } | 159 | } | ||
| 160 | } | 160 | } | ||
| 161 | 161 | ||||
| 162 | - | func TestWeightSameAxisSplitNormalizesSolvedExtentsBeforeInsertion(t *testing.T) { | 162 | + | func TestWeightSameAxisSplitHalvesTargetSolvedExtent(t *testing.T) { |
| 163 | t.Parallel() | 163 | t.Parallel() | ||
| 164 | 164 | ||||
| 165 | tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)), Focus: "a"} | 165 | tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)), Focus: "a"} | ||
| @@ -167,8 +167,8 @@ func TestWeightSameAxisSplitNormalizesSolvedExtentsBeforeInsertion(t *testing.T) | |||||
| 167 | require.NoError(t, tr.Split("b", Right, true, "c", area)) | 167 | require.NoError(t, tr.Split("b", Right, true, "c", area)) | ||
| 168 | 168 | ||||
| 169 | require.Equal(t, 54.0, tr.Root.Children[0].Weight) | 169 | require.Equal(t, 54.0, tr.Root.Children[0].Weight) | ||
| 170 | - | require.Equal(t, 27.0, tr.Root.Children[1].Weight) | 170 | + | require.Equal(t, 13.0, tr.Root.Children[1].Weight) |
| 171 | - | require.Zero(t, tr.Root.Children[2].Weight) | 171 | + | require.Equal(t, 13.0, tr.Root.Children[2].Weight) |
| 172 | placements, ok := Solve(tr.Root, area) | 172 | placements, ok := Solve(tr.Root, area) | ||
| 173 | require.True(t, ok) | 173 | require.True(t, ok) | ||
| 174 | require.Equal(t, map[PaneID]int{"a": 40, "b": 20, "c": 20}, placementWidths(placements)) | 174 | require.Equal(t, map[PaneID]int{"a": 40, "b": 20, "c": 20}, placementWidths(placements)) | ||