refactor(render): remove obsolete layout tree capture
- commit
- b9a93ef80efb21bf7ac3ee9ae381d46741d6b528
- author
- brice · · 2026-07-25 14:31 UTC
- parent
- 5578582
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +99 -102
M internal/usecase/daemon/mouse_panes.go +1 -2
| @@ -43,7 +43,6 @@ func hitTestPlacementLocked(tb *tab, col, row int) (layout.Placement, bool) { | |||
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | type tabLayoutSnapshot struct { | |
| 46 | - | root *layout.Node | |
| 47 | 46 | fingerprint string | |
| 48 | 47 | area domain.Rect | |
| 49 | 48 | focus layout.PaneID | |
| @@ -63,7 +62,7 @@ func solveTabLayoutLocked(tb *tab) tabLayoutSnapshot { | |||
| 63 | 62 | } | |
| 64 | 63 | area := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | |
| 65 | 64 | placements, dividers, ok := layout.SolveWithDividers(tb.tree.Root, area) | |
| 66 | - | return tabLayoutSnapshot{root: tb.tree.Root, fingerprint: layoutFingerprint(tb.tree.Root), area: area, focus: tb.tree.Focus, placements: placements, dividers: dividers, ok: ok} | |
| 65 | + | return tabLayoutSnapshot{fingerprint: layoutFingerprint(tb.tree.Root), area: area, focus: tb.tree.Focus, placements: placements, dividers: dividers, ok: ok} | |
| 67 | 66 | } | |
| 68 | 67 | ||
| 69 | 68 | func layoutFingerprint(root *layout.Node) string { | |
| @@ -43,7 +43,6 @@ func hitTestPlacementLocked(tb *tab, col, row int) (layout.Placement, bool) { | |||||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | type tabLayoutSnapshot struct { | 45 | type tabLayoutSnapshot struct { | ||
| 46 | - | root *layout.Node | |||
| 47 | fingerprint string | 46 | fingerprint string | ||
| 48 | area domain.Rect | 47 | area domain.Rect | ||
| 49 | focus layout.PaneID | 48 | focus layout.PaneID | ||
| @@ -63,7 +62,7 @@ func solveTabLayoutLocked(tb *tab) tabLayoutSnapshot { | |||||
| 63 | } | 62 | } | ||
| 64 | area := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | 63 | area := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | ||
| 65 | placements, dividers, ok := layout.SolveWithDividers(tb.tree.Root, area) | 64 | placements, dividers, ok := layout.SolveWithDividers(tb.tree.Root, area) | ||
| 66 | - | return tabLayoutSnapshot{root: tb.tree.Root, fingerprint: layoutFingerprint(tb.tree.Root), area: area, focus: tb.tree.Focus, placements: placements, dividers: dividers, ok: ok} | 65 | + | return tabLayoutSnapshot{fingerprint: layoutFingerprint(tb.tree.Root), area: area, focus: tb.tree.Focus, placements: placements, dividers: dividers, ok: ok} |
| 67 | } | 66 | } | ||
| 68 | 67 | ||||
| 69 | func layoutFingerprint(root *layout.Node) string { | 68 | func layoutFingerprint(root *layout.Node) string { | ||
M internal/usecase/daemon/picker.go +1 -1
| @@ -630,7 +630,7 @@ func snapshotPickerPreview(tb *tab) picker.Preview { | |||
| 630 | 630 | return preview | |
| 631 | 631 | } | |
| 632 | 632 | state := capturedRenderState{layout: capturedTabLayout{ | |
| 633 | - | root: tb.tree.Clone().Root, area: layoutSnap.area, focus: layoutSnap.focus, | |
| 633 | + | area: layoutSnap.area, focus: layoutSnap.focus, | |
| 634 | 634 | placements: append([]layout.Placement(nil), layoutSnap.placements...), | |
| 635 | 635 | dividers: append([]layout.Divider(nil), layoutSnap.dividers...), | |
| 636 | 636 | fingerprint: layoutSnap.fingerprint, valid: true, | |
| @@ -630,7 +630,7 @@ func snapshotPickerPreview(tb *tab) picker.Preview { | |||||
| 630 | return preview | 630 | return preview | ||
| 631 | } | 631 | } | ||
| 632 | state := capturedRenderState{layout: capturedTabLayout{ | 632 | state := capturedRenderState{layout: capturedTabLayout{ | ||
| 633 | - | root: tb.tree.Clone().Root, area: layoutSnap.area, focus: layoutSnap.focus, | 633 | + | area: layoutSnap.area, focus: layoutSnap.focus, |
| 634 | placements: append([]layout.Placement(nil), layoutSnap.placements...), | 634 | placements: append([]layout.Placement(nil), layoutSnap.placements...), | ||
| 635 | dividers: append([]layout.Divider(nil), layoutSnap.dividers...), | 635 | dividers: append([]layout.Divider(nil), layoutSnap.dividers...), | ||
| 636 | fingerprint: layoutSnap.fingerprint, valid: true, | 636 | fingerprint: layoutSnap.fingerprint, valid: true, | ||
M internal/usecase/daemon/render_capture.go +1 -28
| @@ -25,8 +25,6 @@ type renderCaptureScratch struct { | |||
| 25 | 25 | mru []recentSession | |
| 26 | 26 | ranked []rankedRecent | |
| 27 | 27 | titleIDs []layout.PaneID | |
| 28 | - | treeNodes []layout.Node | |
| 29 | - | treeUsed int | |
| 30 | 28 | receipts []damageReceipt | |
| 31 | 29 | } | |
| 32 | 30 | ||
| @@ -55,7 +53,6 @@ type capturedRenderState struct { | |||
| 55 | 53 | } | |
| 56 | 54 | ||
| 57 | 55 | type capturedTabLayout struct { | |
| 58 | - | root *layout.Node | |
| 59 | 56 | area domain.Rect | |
| 60 | 57 | focus layout.PaneID | |
| 61 | 58 | placements []layout.Placement | |
| @@ -231,17 +228,12 @@ func capturePrimaryRenderState( | |||
| 231 | 228 | layoutSnap := solveTabLayoutLocked(tb) | |
| 232 | 229 | scratch.placements = append(scratch.placements[:0], layoutSnap.placements...) | |
| 233 | 230 | scratch.dividers = append(scratch.dividers[:0], layoutSnap.dividers...) | |
| 234 | - | scratch.treeUsed = 0 | |
| 235 | - | var root *layout.Node | |
| 236 | - | if tb.tree != nil { | |
| 237 | - | root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch) | |
| 238 | - | } | |
| 239 | 231 | state := &scratch.state | |
| 240 | 232 | *state = capturedRenderState{ | |
| 241 | 233 | attachment: ac, lease: lease, reset: reset, bars: bars, theme: bars.theme, | |
| 242 | 234 | styles: request.styles, styleGeneration: request.styleGeneration, | |
| 243 | 235 | overlays: overlays, preview: preview, | |
| 244 | - | layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, dividers: scratch.dividers, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | |
| 236 | + | layout: capturedTabLayout{area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, dividers: scratch.dividers, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | |
| 245 | 237 | floatingGeneration: tb.floating.generation, | |
| 246 | 238 | receipts: scratch.receipts[:0], | |
| 247 | 239 | } | |
| @@ -314,25 +306,6 @@ func copyRankedRecentInto(dst, src []rankedRecent) []rankedRecent { | |||
| 314 | 306 | return append(dst[:0], src...) | |
| 315 | 307 | } | |
| 316 | 308 | ||
| 317 | - | func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node { | |
| 318 | - | if src == nil { | |
| 319 | - | return nil | |
| 320 | - | } | |
| 321 | - | if scratch.treeUsed == len(scratch.treeNodes) { | |
| 322 | - | scratch.treeNodes = append(scratch.treeNodes, layout.Node{}) | |
| 323 | - | } | |
| 324 | - | index := scratch.treeUsed | |
| 325 | - | scratch.treeUsed++ | |
| 326 | - | dst := &scratch.treeNodes[index] | |
| 327 | - | children := dst.Children[:0] | |
| 328 | - | *dst = *src | |
| 329 | - | dst.Children = children | |
| 330 | - | for _, child := range src.Children { | |
| 331 | - | dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch)) | |
| 332 | - | } | |
| 333 | - | return dst | |
| 334 | - | } | |
| 335 | - | ||
| 336 | 309 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | |
| 337 | 310 | style, hasStyle := p.screen.CursorStyle() | |
| 338 | 311 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive || overlays.noticesOverlayActive | |
| @@ -25,8 +25,6 @@ type renderCaptureScratch struct { | |||||
| 25 | mru []recentSession | 25 | mru []recentSession | ||
| 26 | ranked []rankedRecent | 26 | ranked []rankedRecent | ||
| 27 | titleIDs []layout.PaneID | 27 | titleIDs []layout.PaneID | ||
| 28 | - | treeNodes []layout.Node | |||
| 29 | - | treeUsed int | |||
| 30 | receipts []damageReceipt | 28 | receipts []damageReceipt | ||
| 31 | } | 29 | } | ||
| 32 | 30 | ||||
| @@ -55,7 +53,6 @@ type capturedRenderState struct { | |||||
| 55 | } | 53 | } | ||
| 56 | 54 | ||||
| 57 | type capturedTabLayout struct { | 55 | type capturedTabLayout struct { | ||
| 58 | - | root *layout.Node | |||
| 59 | area domain.Rect | 56 | area domain.Rect | ||
| 60 | focus layout.PaneID | 57 | focus layout.PaneID | ||
| 61 | placements []layout.Placement | 58 | placements []layout.Placement | ||
| @@ -231,17 +228,12 @@ func capturePrimaryRenderState( | |||||
| 231 | layoutSnap := solveTabLayoutLocked(tb) | 228 | layoutSnap := solveTabLayoutLocked(tb) | ||
| 232 | scratch.placements = append(scratch.placements[:0], layoutSnap.placements...) | 229 | scratch.placements = append(scratch.placements[:0], layoutSnap.placements...) | ||
| 233 | scratch.dividers = append(scratch.dividers[:0], layoutSnap.dividers...) | 230 | scratch.dividers = append(scratch.dividers[:0], layoutSnap.dividers...) | ||
| 234 | - | scratch.treeUsed = 0 | |||
| 235 | - | var root *layout.Node | |||
| 236 | - | if tb.tree != nil { | |||
| 237 | - | root = cloneLayoutNodeIntoScratch(tb.tree.Root, scratch) | |||
| 238 | - | } | |||
| 239 | state := &scratch.state | 231 | state := &scratch.state | ||
| 240 | *state = capturedRenderState{ | 232 | *state = capturedRenderState{ | ||
| 241 | attachment: ac, lease: lease, reset: reset, bars: bars, theme: bars.theme, | 233 | attachment: ac, lease: lease, reset: reset, bars: bars, theme: bars.theme, | ||
| 242 | styles: request.styles, styleGeneration: request.styleGeneration, | 234 | styles: request.styles, styleGeneration: request.styleGeneration, | ||
| 243 | overlays: overlays, preview: preview, | 235 | overlays: overlays, preview: preview, | ||
| 244 | - | layout: capturedTabLayout{root: root, area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, dividers: scratch.dividers, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, | 236 | + | layout: capturedTabLayout{area: layoutSnap.area, focus: layoutSnap.focus, placements: scratch.placements, dividers: scratch.dividers, fingerprint: layoutSnap.fingerprint, valid: layoutSnap.ok}, |
| 245 | floatingGeneration: tb.floating.generation, | 237 | floatingGeneration: tb.floating.generation, | ||
| 246 | receipts: scratch.receipts[:0], | 238 | receipts: scratch.receipts[:0], | ||
| 247 | } | 239 | } | ||
| @@ -314,25 +306,6 @@ func copyRankedRecentInto(dst, src []rankedRecent) []rankedRecent { | |||||
| 314 | return append(dst[:0], src...) | 306 | return append(dst[:0], src...) | ||
| 315 | } | 307 | } | ||
| 316 | 308 | ||||
| 317 | - | func cloneLayoutNodeIntoScratch(src *layout.Node, scratch *renderCaptureScratch) *layout.Node { | |||
| 318 | - | if src == nil { | |||
| 319 | - | return nil | |||
| 320 | - | } | |||
| 321 | - | if scratch.treeUsed == len(scratch.treeNodes) { | |||
| 322 | - | scratch.treeNodes = append(scratch.treeNodes, layout.Node{}) | |||
| 323 | - | } | |||
| 324 | - | index := scratch.treeUsed | |||
| 325 | - | scratch.treeUsed++ | |||
| 326 | - | dst := &scratch.treeNodes[index] | |||
| 327 | - | children := dst.Children[:0] | |||
| 328 | - | *dst = *src | |||
| 329 | - | dst.Children = children | |||
| 330 | - | for _, child := range src.Children { | |||
| 331 | - | dst.Children = append(dst.Children, cloneLayoutNodeIntoScratch(child, scratch)) | |||
| 332 | - | } | |||
| 333 | - | return dst | |||
| 334 | - | } | |||
| 335 | - | ||||
| 336 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | 309 | func captureCursorInputsLocked(p *pane, content domain.Rect, overlays capturedOverlayRenderState) capturedCursorInputs { | ||
| 337 | style, hasStyle := p.screen.CursorStyle() | 310 | style, hasStyle := p.screen.CursorStyle() | ||
| 338 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive || overlays.noticesOverlayActive | 311 | hidden := overlays.copyActive || overlays.copySearchActive || overlays.pickerActive || overlays.paletteActive || overlays.promptActive || overlays.noticesOverlayActive | ||
M internal/usecase/daemon/render_divider_geometry_test.go +65 -37
| @@ -9,46 +9,74 @@ import ( | |||
| 9 | 9 | "github.com/stretchr/testify/require" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | - | // TestComposeFrameDividerFollowsWeights proves the rendered divider glyph | |
| 13 | - | // tracks the weight-aware pane geometry from layout.Solve, rather than an | |
| 14 | - | // equal-split recomputation. On a 61-wide area, equal weights place the | |
| 15 | - | // divider at x=30; a 2:1 weight split moves the real gap to x=40. The | |
| 16 | - | // divider must move with it and must not linger at the old column. | |
| 12 | + | // TestComposeFrameDividerFollowsWeights proves rendered divider glyphs track | |
| 13 | + | // weight-aware pane geometry rather than an equal-split recomputation. | |
| 17 | 14 | func TestComposeFrameDividerFollowsWeights(t *testing.T) { | |
| 18 | - | root := &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{ | |
| 19 | - | {Kind: layout.Leaf, Leaf: "left", Weight: 1}, | |
| 20 | - | {Kind: layout.Leaf, Leaf: "right", Weight: 1}, | |
| 21 | - | }} | |
| 22 | - | area := domain.Rect{Width: 61, Height: 5} | |
| 23 | - | ||
| 24 | - | stateFor := func() capturedRenderState { | |
| 25 | - | placements, dividers, ok := layout.SolveWithDividers(root, area) | |
| 26 | - | require.True(t, ok) | |
| 27 | - | panes := make([]capturedPaneRenderState, 0, len(placements)) | |
| 28 | - | for _, placement := range placements { | |
| 29 | - | frame := cachePaneFrame(placement.Content.Width, placement.Content.Height, rune(placement.ID[0])) | |
| 30 | - | panes = append(panes, capturedPaneRenderState{ | |
| 31 | - | id: placement.ID, frame: frame, placement: placement, focused: placement.ID == "left", | |
| 32 | - | damage: []renderer.Damage{renderer.FullRedraw()}, | |
| 33 | - | }) | |
| 34 | - | } | |
| 35 | - | return capturedRenderState{ | |
| 36 | - | reset: true, | |
| 37 | - | layout: capturedTabLayout{root: root, area: area, focus: "left", placements: placements, dividers: dividers, fingerprint: layoutFingerprint(root), valid: true}, | |
| 38 | - | panes: panes, styles: resolveStyles(nil), | |
| 39 | - | } | |
| 15 | + | tests := []struct { | |
| 16 | + | name string | |
| 17 | + | direction layout.SplitDir | |
| 18 | + | area domain.Rect | |
| 19 | + | glyph rune | |
| 20 | + | equalX, equalY int | |
| 21 | + | weightedX, weightedY int | |
| 22 | + | }{ | |
| 23 | + | { | |
| 24 | + | name: "horizontal split", | |
| 25 | + | direction: layout.Horizontal, | |
| 26 | + | area: domain.Rect{Width: 61, Height: 5}, | |
| 27 | + | glyph: '│', | |
| 28 | + | equalX: 30, | |
| 29 | + | equalY: 1, | |
| 30 | + | weightedX: 40, | |
| 31 | + | weightedY: 1, | |
| 32 | + | }, | |
| 33 | + | { | |
| 34 | + | name: "vertical split", | |
| 35 | + | direction: layout.Vertical, | |
| 36 | + | area: domain.Rect{Width: 41, Height: 10}, | |
| 37 | + | glyph: '─', | |
| 38 | + | equalX: 1, | |
| 39 | + | equalY: 6, | |
| 40 | + | weightedX: 1, | |
| 41 | + | weightedY: 7, | |
| 42 | + | }, | |
| 40 | 43 | } | |
| 41 | 44 | ||
| 42 | - | equal := stateFor() | |
| 43 | - | out := composeFrame(equal, composeCacheInput{}) | |
| 44 | - | require.Equal(t, '│', out.frame.At(30, 1).Rune, "equal weights must place the divider at the equal-split column") | |
| 45 | + | for _, tt := range tests { | |
| 46 | + | t.Run(tt.name, func(t *testing.T) { | |
| 47 | + | root := &layout.Node{Kind: layout.Split, Dir: tt.direction, Children: []*layout.Node{ | |
| 48 | + | {Kind: layout.Leaf, Leaf: "first", Weight: 1}, | |
| 49 | + | {Kind: layout.Leaf, Leaf: "second", Weight: 1}, | |
| 50 | + | }} | |
| 51 | + | stateFor := func() capturedRenderState { | |
| 52 | + | placements, dividers, ok := layout.SolveWithDividers(root, tt.area) | |
| 53 | + | require.True(t, ok) | |
| 54 | + | panes := make([]capturedPaneRenderState, 0, len(placements)) | |
| 55 | + | for _, placement := range placements { | |
| 56 | + | frame := cachePaneFrame(placement.Content.Width, placement.Content.Height, rune(placement.ID[0])) | |
| 57 | + | panes = append(panes, capturedPaneRenderState{ | |
| 58 | + | id: placement.ID, frame: frame, placement: placement, focused: placement.ID == "first", | |
| 59 | + | damage: []renderer.Damage{renderer.FullRedraw()}, | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | return capturedRenderState{ | |
| 63 | + | reset: true, | |
| 64 | + | layout: capturedTabLayout{area: tt.area, focus: "first", placements: placements, dividers: dividers, fingerprint: layoutFingerprint(root), valid: true}, | |
| 65 | + | panes: panes, styles: resolveStyles(nil), | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | equal := stateFor() | |
| 70 | + | out := composeFrame(equal, composeCacheInput{}) | |
| 71 | + | require.Equal(t, tt.glyph, out.frame.At(tt.equalX, tt.equalY).Rune, "equal weights must place the divider at the equal-split gap") | |
| 45 | 72 | ||
| 46 | - | root.Children[0].Weight = 2 | |
| 47 | - | weighted := stateFor() | |
| 48 | - | require.NotEqual(t, equal.layout.fingerprint, weighted.layout.fingerprint) | |
| 49 | - | require.Equal(t, 41, weighted.layout.placements[1].Content.X, "sanity check: left pane is 40 wide, so the right pane starts at x=41") | |
| 73 | + | root.Children[0].Weight = 2 | |
| 74 | + | weighted := stateFor() | |
| 75 | + | require.NotEqual(t, equal.layout.fingerprint, weighted.layout.fingerprint) | |
| 50 | 76 | ||
| 51 | - | out = composeFrame(weighted, out.cache, composeCacheInput{}) | |
| 52 | - | require.Equal(t, '│', out.frame.At(40, 1).Rune, "the divider must follow the weight-derived gap") | |
| 53 | - | require.NotEqual(t, '│', out.frame.At(30, 1).Rune, "the divider must not linger at the old equal-split column") | |
| 77 | + | out = composeFrame(weighted, out.cache, composeCacheInput{}) | |
| 78 | + | require.Equal(t, tt.glyph, out.frame.At(tt.weightedX, tt.weightedY).Rune, "the divider must follow the weight-derived gap") | |
| 79 | + | require.NotEqual(t, tt.glyph, out.frame.At(tt.equalX, tt.equalY).Rune, "the divider must not linger at the old equal-split gap") | |
| 80 | + | }) | |
| 81 | + | } | |
| 54 | 82 | } | |
| @@ -9,46 +9,74 @@ import ( | |||||
| 9 | "github.com/stretchr/testify/require" | 9 | "github.com/stretchr/testify/require" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | - | // TestComposeFrameDividerFollowsWeights proves the rendered divider glyph | 12 | + | // TestComposeFrameDividerFollowsWeights proves rendered divider glyphs track |
| 13 | - | // tracks the weight-aware pane geometry from layout.Solve, rather than an | 13 | + | // weight-aware pane geometry rather than an equal-split recomputation. |
| 14 | - | // equal-split recomputation. On a 61-wide area, equal weights place the | |||
| 15 | - | // divider at x=30; a 2:1 weight split moves the real gap to x=40. The | |||
| 16 | - | // divider must move with it and must not linger at the old column. | |||
| 17 | func TestComposeFrameDividerFollowsWeights(t *testing.T) { | 14 | func TestComposeFrameDividerFollowsWeights(t *testing.T) { | ||
| 18 | - | root := &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{ | 15 | + | tests := []struct { |
| 19 | - | {Kind: layout.Leaf, Leaf: "left", Weight: 1}, | 16 | + | name string |
| 20 | - | {Kind: layout.Leaf, Leaf: "right", Weight: 1}, | 17 | + | direction layout.SplitDir |
| 21 | - | }} | 18 | + | area domain.Rect |
| 22 | - | area := domain.Rect{Width: 61, Height: 5} | 19 | + | glyph rune |
| 23 | - | 20 | + | equalX, equalY int | |
| 24 | - | stateFor := func() capturedRenderState { | 21 | + | weightedX, weightedY int |
| 25 | - | placements, dividers, ok := layout.SolveWithDividers(root, area) | 22 | + | }{ |
| 26 | - | require.True(t, ok) | 23 | + | { |
| 27 | - | panes := make([]capturedPaneRenderState, 0, len(placements)) | 24 | + | name: "horizontal split", |
| 28 | - | for _, placement := range placements { | 25 | + | direction: layout.Horizontal, |
| 29 | - | frame := cachePaneFrame(placement.Content.Width, placement.Content.Height, rune(placement.ID[0])) | 26 | + | area: domain.Rect{Width: 61, Height: 5}, |
| 30 | - | panes = append(panes, capturedPaneRenderState{ | 27 | + | glyph: '│', |
| 31 | - | id: placement.ID, frame: frame, placement: placement, focused: placement.ID == "left", | 28 | + | equalX: 30, |
| 32 | - | damage: []renderer.Damage{renderer.FullRedraw()}, | 29 | + | equalY: 1, |
| 33 | - | }) | 30 | + | weightedX: 40, |
| 34 | - | } | 31 | + | weightedY: 1, |
| 35 | - | return capturedRenderState{ | 32 | + | }, |
| 36 | - | reset: true, | 33 | + | { |
| 37 | - | layout: capturedTabLayout{root: root, area: area, focus: "left", placements: placements, dividers: dividers, fingerprint: layoutFingerprint(root), valid: true}, | 34 | + | name: "vertical split", |
| 38 | - | panes: panes, styles: resolveStyles(nil), | 35 | + | direction: layout.Vertical, |
| 39 | - | } | 36 | + | area: domain.Rect{Width: 41, Height: 10}, |
| 37 | + | glyph: '─', | |||
| 38 | + | equalX: 1, | |||
| 39 | + | equalY: 6, | |||
| 40 | + | weightedX: 1, | |||
| 41 | + | weightedY: 7, | |||
| 42 | + | }, | |||
| 40 | } | 43 | } | ||
| 41 | 44 | ||||
| 42 | - | equal := stateFor() | 45 | + | for _, tt := range tests { |
| 43 | - | out := composeFrame(equal, composeCacheInput{}) | 46 | + | t.Run(tt.name, func(t *testing.T) { |
| 44 | - | require.Equal(t, '│', out.frame.At(30, 1).Rune, "equal weights must place the divider at the equal-split column") | 47 | + | root := &layout.Node{Kind: layout.Split, Dir: tt.direction, Children: []*layout.Node{ |
| 48 | + | {Kind: layout.Leaf, Leaf: "first", Weight: 1}, | |||
| 49 | + | {Kind: layout.Leaf, Leaf: "second", Weight: 1}, | |||
| 50 | + | }} | |||
| 51 | + | stateFor := func() capturedRenderState { | |||
| 52 | + | placements, dividers, ok := layout.SolveWithDividers(root, tt.area) | |||
| 53 | + | require.True(t, ok) | |||
| 54 | + | panes := make([]capturedPaneRenderState, 0, len(placements)) | |||
| 55 | + | for _, placement := range placements { | |||
| 56 | + | frame := cachePaneFrame(placement.Content.Width, placement.Content.Height, rune(placement.ID[0])) | |||
| 57 | + | panes = append(panes, capturedPaneRenderState{ | |||
| 58 | + | id: placement.ID, frame: frame, placement: placement, focused: placement.ID == "first", | |||
| 59 | + | damage: []renderer.Damage{renderer.FullRedraw()}, | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | return capturedRenderState{ | |||
| 63 | + | reset: true, | |||
| 64 | + | layout: capturedTabLayout{area: tt.area, focus: "first", placements: placements, dividers: dividers, fingerprint: layoutFingerprint(root), valid: true}, | |||
| 65 | + | panes: panes, styles: resolveStyles(nil), | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | equal := stateFor() | |||
| 70 | + | out := composeFrame(equal, composeCacheInput{}) | |||
| 71 | + | require.Equal(t, tt.glyph, out.frame.At(tt.equalX, tt.equalY).Rune, "equal weights must place the divider at the equal-split gap") | |||
| 45 | 72 | ||||
| 46 | - | root.Children[0].Weight = 2 | 73 | + | root.Children[0].Weight = 2 |
| 47 | - | weighted := stateFor() | 74 | + | weighted := stateFor() |
| 48 | - | require.NotEqual(t, equal.layout.fingerprint, weighted.layout.fingerprint) | 75 | + | require.NotEqual(t, equal.layout.fingerprint, weighted.layout.fingerprint) |
| 49 | - | require.Equal(t, 41, weighted.layout.placements[1].Content.X, "sanity check: left pane is 40 wide, so the right pane starts at x=41") | |||
| 50 | 76 | ||||
| 51 | - | out = composeFrame(weighted, out.cache, composeCacheInput{}) | 77 | + | out = composeFrame(weighted, out.cache, composeCacheInput{}) |
| 52 | - | require.Equal(t, '│', out.frame.At(40, 1).Rune, "the divider must follow the weight-derived gap") | 78 | + | require.Equal(t, tt.glyph, out.frame.At(tt.weightedX, tt.weightedY).Rune, "the divider must follow the weight-derived gap") |
| 53 | - | require.NotEqual(t, '│', out.frame.At(30, 1).Rune, "the divider must not linger at the old equal-split column") | 79 | + | require.NotEqual(t, tt.glyph, out.frame.At(tt.equalX, tt.equalY).Rune, "the divider must not linger at the old equal-split gap") |
| 80 | + | }) | |||
| 81 | + | } | |||
| 54 | } | 82 | } | ||
M internal/usecase/daemon/render_pipeline_cache_test.go +8 -9
| @@ -147,7 +147,7 @@ func TestLayoutFingerprintWeightChangeInvalidatesGeometryCache(t *testing.T) { | |||
| 147 | 147 | } | |
| 148 | 148 | return capturedRenderState{ | |
| 149 | 149 | reset: reset, | |
| 150 | - | layout: capturedTabLayout{root: root, area: domain.Rect{Width: 61, Height: 5}, focus: "left", placements: placements, fingerprint: layoutFingerprint(root), valid: true}, | |
| 150 | + | layout: capturedTabLayout{area: domain.Rect{Width: 61, Height: 5}, focus: "left", placements: placements, fingerprint: layoutFingerprint(root), valid: true}, | |
| 151 | 151 | panes: panes, styles: resolveStyles(nil), | |
| 152 | 152 | } | |
| 153 | 153 | } | |
| @@ -260,16 +260,15 @@ func cachedSplitState(fingerprint string, focus layout.PaneID, direction layout. | |||
| 260 | 260 | left, right := layout.PaneID("left"), layout.PaneID("right") | |
| 261 | 261 | area := domain.Rect{Width: 41, Height: 5} | |
| 262 | 262 | root := &layout.Node{Kind: layout.Split, Dir: direction, Children: []*layout.Node{layout.NewLeaf(left), layout.NewLeaf(right)}} | |
| 263 | - | placements := []layout.Placement{{ID: left, Content: domain.Rect{Width: 20, Height: 5}}, {ID: right, Content: domain.Rect{X: 21, Width: 20, Height: 5}}} | |
| 264 | - | leftFrame, rightFrame := cachePaneFrame(20, 5, 'L'), cachePaneFrame(20, 5, 'R') | |
| 265 | - | if direction == layout.Vertical { | |
| 266 | - | placements = []layout.Placement{{ID: left, Content: domain.Rect{Width: 41, Height: 2}}, {ID: right, Content: domain.Rect{Y: 3, Width: 41, Height: 2}}} | |
| 267 | - | leftFrame, rightFrame = cachePaneFrame(41, 2, 'L'), cachePaneFrame(41, 2, 'R') | |
| 263 | + | placements, dividers, ok := layout.SolveWithDividers(root, area) | |
| 264 | + | if !ok { | |
| 265 | + | panic("cached split fixture has invalid geometry") | |
| 268 | 266 | } | |
| 269 | - | _, dividers, _ := layout.SolveWithDividers(root, area) | |
| 267 | + | leftFrame := cachePaneFrame(placements[0].Content.Width, placements[0].Content.Height, 'L') | |
| 268 | + | rightFrame := cachePaneFrame(placements[1].Content.Width, placements[1].Content.Height, 'R') | |
| 270 | 269 | return capturedRenderState{ | |
| 271 | 270 | reset: true, | |
| 272 | - | layout: capturedTabLayout{root: root, area: area, focus: focus, placements: placements, dividers: dividers, fingerprint: fingerprint, valid: true}, | |
| 271 | + | layout: capturedTabLayout{area: area, focus: focus, placements: placements, dividers: dividers, fingerprint: fingerprint, valid: true}, | |
| 273 | 272 | panes: []capturedPaneRenderState{{id: left, frame: leftFrame, placement: placements[0], focused: focus == left, damage: []renderer.Damage{renderer.FullRedraw()}}, {id: right, frame: rightFrame, placement: placements[1], focused: focus == right, damage: []renderer.Damage{renderer.FullRedraw()}}}, | |
| 274 | 273 | theme: theme, | |
| 275 | 274 | styles: themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles, | |
| @@ -284,7 +283,7 @@ func cachedStackTitleState(title string, generation uint64, reset bool) captured | |||
| 284 | 283 | } | |
| 285 | 284 | return capturedRenderState{ | |
| 286 | 285 | reset: reset, | |
| 287 | - | layout: capturedTabLayout{root: &layout.Node{Kind: layout.Stack, Children: []*layout.Node{layout.NewLeaf(first), layout.NewLeaf(second)}, Expanded: second}, area: domain.Rect{Width: 20, Height: 5}, focus: second, placements: placements, fingerprint: "stack", valid: true}, | |
| 286 | + | layout: capturedTabLayout{area: domain.Rect{Width: 20, Height: 5}, focus: second, placements: placements, fingerprint: "stack", valid: true}, | |
| 288 | 287 | panes: []capturedPaneRenderState{{id: first, title: title, titleGeneration: generation, placement: placements[0]}, {id: second, frame: cachePaneFrame(20, 3, 'E'), title: "second", titleGeneration: 1, placement: placements[1], focused: true, damage: []renderer.Damage{renderer.FullRedraw()}}}, | |
| 289 | 288 | styles: resolveStyles(nil), | |
| 290 | 289 | } | |
| @@ -147,7 +147,7 @@ func TestLayoutFingerprintWeightChangeInvalidatesGeometryCache(t *testing.T) { | |||||
| 147 | } | 147 | } | ||
| 148 | return capturedRenderState{ | 148 | return capturedRenderState{ | ||
| 149 | reset: reset, | 149 | reset: reset, | ||
| 150 | - | layout: capturedTabLayout{root: root, area: domain.Rect{Width: 61, Height: 5}, focus: "left", placements: placements, fingerprint: layoutFingerprint(root), valid: true}, | 150 | + | layout: capturedTabLayout{area: domain.Rect{Width: 61, Height: 5}, focus: "left", placements: placements, fingerprint: layoutFingerprint(root), valid: true}, |
| 151 | panes: panes, styles: resolveStyles(nil), | 151 | panes: panes, styles: resolveStyles(nil), | ||
| 152 | } | 152 | } | ||
| 153 | } | 153 | } | ||
| @@ -260,16 +260,15 @@ func cachedSplitState(fingerprint string, focus layout.PaneID, direction layout. | |||||
| 260 | left, right := layout.PaneID("left"), layout.PaneID("right") | 260 | left, right := layout.PaneID("left"), layout.PaneID("right") | ||
| 261 | area := domain.Rect{Width: 41, Height: 5} | 261 | area := domain.Rect{Width: 41, Height: 5} | ||
| 262 | root := &layout.Node{Kind: layout.Split, Dir: direction, Children: []*layout.Node{layout.NewLeaf(left), layout.NewLeaf(right)}} | 262 | root := &layout.Node{Kind: layout.Split, Dir: direction, Children: []*layout.Node{layout.NewLeaf(left), layout.NewLeaf(right)}} | ||
| 263 | - | placements := []layout.Placement{{ID: left, Content: domain.Rect{Width: 20, Height: 5}}, {ID: right, Content: domain.Rect{X: 21, Width: 20, Height: 5}}} | 263 | + | placements, dividers, ok := layout.SolveWithDividers(root, area) |
| 264 | - | leftFrame, rightFrame := cachePaneFrame(20, 5, 'L'), cachePaneFrame(20, 5, 'R') | 264 | + | if !ok { |
| 265 | - | if direction == layout.Vertical { | 265 | + | panic("cached split fixture has invalid geometry") |
| 266 | - | placements = []layout.Placement{{ID: left, Content: domain.Rect{Width: 41, Height: 2}}, {ID: right, Content: domain.Rect{Y: 3, Width: 41, Height: 2}}} | |||
| 267 | - | leftFrame, rightFrame = cachePaneFrame(41, 2, 'L'), cachePaneFrame(41, 2, 'R') | |||
| 268 | } | 266 | } | ||
| 269 | - | _, dividers, _ := layout.SolveWithDividers(root, area) | 267 | + | leftFrame := cachePaneFrame(placements[0].Content.Width, placements[0].Content.Height, 'L') |
| 268 | + | rightFrame := cachePaneFrame(placements[1].Content.Width, placements[1].Content.Height, 'R') | |||
| 270 | return capturedRenderState{ | 269 | return capturedRenderState{ | ||
| 271 | reset: true, | 270 | reset: true, | ||
| 272 | - | layout: capturedTabLayout{root: root, area: area, focus: focus, placements: placements, dividers: dividers, fingerprint: fingerprint, valid: true}, | 271 | + | layout: capturedTabLayout{area: area, focus: focus, placements: placements, dividers: dividers, fingerprint: fingerprint, valid: true}, |
| 273 | panes: []capturedPaneRenderState{{id: left, frame: leftFrame, placement: placements[0], focused: focus == left, damage: []renderer.Damage{renderer.FullRedraw()}}, {id: right, frame: rightFrame, placement: placements[1], focused: focus == right, damage: []renderer.Damage{renderer.FullRedraw()}}}, | 272 | panes: []capturedPaneRenderState{{id: left, frame: leftFrame, placement: placements[0], focused: focus == left, damage: []renderer.Damage{renderer.FullRedraw()}}, {id: right, frame: rightFrame, placement: placements[1], focused: focus == right, damage: []renderer.Damage{renderer.FullRedraw()}}}, | ||
| 274 | theme: theme, | 273 | theme: theme, | ||
| 275 | styles: themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles, | 274 | styles: themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles, | ||
| @@ -284,7 +283,7 @@ func cachedStackTitleState(title string, generation uint64, reset bool) captured | |||||
| 284 | } | 283 | } | ||
| 285 | return capturedRenderState{ | 284 | return capturedRenderState{ | ||
| 286 | reset: reset, | 285 | reset: reset, | ||
| 287 | - | layout: capturedTabLayout{root: &layout.Node{Kind: layout.Stack, Children: []*layout.Node{layout.NewLeaf(first), layout.NewLeaf(second)}, Expanded: second}, area: domain.Rect{Width: 20, Height: 5}, focus: second, placements: placements, fingerprint: "stack", valid: true}, | 286 | + | layout: capturedTabLayout{area: domain.Rect{Width: 20, Height: 5}, focus: second, placements: placements, fingerprint: "stack", valid: true}, |
| 288 | panes: []capturedPaneRenderState{{id: first, title: title, titleGeneration: generation, placement: placements[0]}, {id: second, frame: cachePaneFrame(20, 3, 'E'), title: "second", titleGeneration: 1, placement: placements[1], focused: true, damage: []renderer.Damage{renderer.FullRedraw()}}}, | 287 | panes: []capturedPaneRenderState{{id: first, title: title, titleGeneration: generation, placement: placements[0]}, {id: second, frame: cachePaneFrame(20, 3, 'E'), title: "second", titleGeneration: 1, placement: placements[1], focused: true, damage: []renderer.Damage{renderer.FullRedraw()}}}, | ||
| 289 | styles: resolveStyles(nil), | 288 | styles: resolveStyles(nil), | ||
| 290 | } | 289 | } | ||
M internal/usecase/layout/dividers_test.go +23 -25
| @@ -7,22 +7,6 @@ import ( | |||
| 7 | 7 | "github.com/stretchr/testify/require" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | - | // countGaps returns the number of sibling gaps in the tree, i.e. the number | |
| 11 | - | // of divider rects SolveWithDividers is expected to produce. | |
| 12 | - | func countGaps(n *Node) int { | |
| 13 | - | if n == nil || n.Kind != Split { | |
| 14 | - | return 0 | |
| 15 | - | } | |
| 16 | - | total := 0 | |
| 17 | - | if len(n.Children) > 1 { | |
| 18 | - | total = len(n.Children) - 1 | |
| 19 | - | } | |
| 20 | - | for _, child := range n.Children { | |
| 21 | - | total += countGaps(child) | |
| 22 | - | } | |
| 23 | - | return total | |
| 24 | - | } | |
| 25 | - | ||
| 26 | 10 | func rectsOverlap(a, b domain.Rect) bool { | |
| 27 | 11 | if a.Width <= 0 || a.Height <= 0 || b.Width <= 0 || b.Height <= 0 { | |
| 28 | 12 | return false | |
| @@ -34,9 +18,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 34 | 18 | t.Parallel() | |
| 35 | 19 | ||
| 36 | 20 | tests := []struct { | |
| 37 | - | name string | |
| 38 | - | root *Node | |
| 39 | - | area domain.Rect | |
| 21 | + | name string | |
| 22 | + | root *Node | |
| 23 | + | area domain.Rect | |
| 24 | + | expected []Divider | |
| 40 | 25 | }{ | |
| 41 | 26 | { | |
| 42 | 27 | name: "equal weights", | |
| @@ -44,7 +29,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 44 | 29 | {Kind: Leaf, Leaf: "a", Weight: 1}, | |
| 45 | 30 | {Kind: Leaf, Leaf: "b", Weight: 1}, | |
| 46 | 31 | }}, | |
| 47 | - | area: domain.Rect{Width: 61, Height: 10}, | |
| 32 | + | area: domain.Rect{Width: 61, Height: 10}, | |
| 33 | + | expected: []Divider{{Rect: domain.Rect{X: 30, Width: 1, Height: 10}, Dir: Horizontal}}, | |
| 48 | 34 | }, | |
| 49 | 35 | { | |
| 50 | 36 | name: "unequal weights 2:1", | |
| @@ -52,7 +38,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 52 | 38 | {Kind: Leaf, Leaf: "a", Weight: 2}, | |
| 53 | 39 | {Kind: Leaf, Leaf: "b", Weight: 1}, | |
| 54 | 40 | }}, | |
| 55 | - | area: domain.Rect{Width: 61, Height: 10}, | |
| 41 | + | area: domain.Rect{Width: 61, Height: 10}, | |
| 42 | + | expected: []Divider{{Rect: domain.Rect{X: 40, Width: 1, Height: 10}, Dir: Horizontal}}, | |
| 56 | 43 | }, | |
| 57 | 44 | { | |
| 58 | 45 | name: "unequal weights 1:3", | |
| @@ -60,7 +47,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 60 | 47 | {Kind: Leaf, Leaf: "a", Weight: 1}, | |
| 61 | 48 | {Kind: Leaf, Leaf: "b", Weight: 3}, | |
| 62 | 49 | }}, | |
| 63 | - | area: domain.Rect{Width: 81, Height: 10}, | |
| 50 | + | area: domain.Rect{Width: 81, Height: 10}, | |
| 51 | + | expected: []Divider{{Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}}, | |
| 64 | 52 | }, | |
| 65 | 53 | { | |
| 66 | 54 | name: "nested split", | |
| @@ -72,6 +60,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 72 | 60 | }}, | |
| 73 | 61 | }}, | |
| 74 | 62 | area: domain.Rect{Width: 61, Height: 20}, | |
| 63 | + | expected: []Divider{ | |
| 64 | + | {Rect: domain.Rect{X: 30, Width: 1, Height: 20}, Dir: Horizontal}, | |
| 65 | + | {Rect: domain.Rect{X: 31, Y: 10, Width: 30, Height: 1}, Dir: Vertical}, | |
| 66 | + | }, | |
| 75 | 67 | }, | |
| 76 | 68 | { | |
| 77 | 69 | name: "three or more children", | |
| @@ -81,6 +73,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 81 | 73 | {Kind: Leaf, Leaf: "c", Weight: 1}, | |
| 82 | 74 | }}, | |
| 83 | 75 | area: domain.Rect{Width: 62, Height: 10}, | |
| 76 | + | expected: []Divider{ | |
| 77 | + | {Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}, | |
| 78 | + | {Rect: domain.Rect{X: 41, Width: 1, Height: 10}, Dir: Horizontal}, | |
| 79 | + | }, | |
| 84 | 80 | }, | |
| 85 | 81 | { | |
| 86 | 82 | name: "minimum pins a child", | |
| @@ -88,7 +84,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 88 | 84 | {Kind: Leaf, Leaf: "a", Weight: 1}, | |
| 89 | 85 | {Kind: Leaf, Leaf: "b", Weight: 100}, | |
| 90 | 86 | }}, | |
| 91 | - | area: domain.Rect{Width: 61, Height: 10}, | |
| 87 | + | area: domain.Rect{Width: 61, Height: 10}, | |
| 88 | + | expected: []Divider{{Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}}, | |
| 92 | 89 | }, | |
| 93 | 90 | { | |
| 94 | 91 | name: "vertical split", | |
| @@ -96,7 +93,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 96 | 93 | {Kind: Leaf, Leaf: "a", Weight: 1}, | |
| 97 | 94 | {Kind: Leaf, Leaf: "b", Weight: 2}, | |
| 98 | 95 | }}, | |
| 99 | - | area: domain.Rect{Width: 41, Height: 10}, | |
| 96 | + | area: domain.Rect{Width: 41, Height: 10}, | |
| 97 | + | expected: []Divider{{Rect: domain.Rect{Y: 3, Width: 41, Height: 1}, Dir: Vertical}}, | |
| 100 | 98 | }, | |
| 101 | 99 | } | |
| 102 | 100 | ||
| @@ -110,7 +108,7 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||
| 110 | 108 | require.True(t, solveOK) | |
| 111 | 109 | require.Equal(t, solved, placements, "collecting dividers must not change the solved placements") | |
| 112 | 110 | ||
| 113 | - | require.Len(t, dividers, countGaps(tt.root), "expected exactly one divider per sibling gap") | |
| 111 | + | require.Equal(t, tt.expected, dividers, "divider geometry and direction must match the solved gaps exactly") | |
| 114 | 112 | ||
| 115 | 113 | for _, d := range dividers { | |
| 116 | 114 | require.True(t, d.Rect.Width == 1 || d.Rect.Height == 1, "divider rect must be a single row or column") | |
| @@ -7,22 +7,6 @@ import ( | |||||
| 7 | "github.com/stretchr/testify/require" | 7 | "github.com/stretchr/testify/require" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | - | // countGaps returns the number of sibling gaps in the tree, i.e. the number | |||
| 11 | - | // of divider rects SolveWithDividers is expected to produce. | |||
| 12 | - | func countGaps(n *Node) int { | |||
| 13 | - | if n == nil || n.Kind != Split { | |||
| 14 | - | return 0 | |||
| 15 | - | } | |||
| 16 | - | total := 0 | |||
| 17 | - | if len(n.Children) > 1 { | |||
| 18 | - | total = len(n.Children) - 1 | |||
| 19 | - | } | |||
| 20 | - | for _, child := range n.Children { | |||
| 21 | - | total += countGaps(child) | |||
| 22 | - | } | |||
| 23 | - | return total | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | func rectsOverlap(a, b domain.Rect) bool { | 10 | func rectsOverlap(a, b domain.Rect) bool { | ||
| 27 | if a.Width <= 0 || a.Height <= 0 || b.Width <= 0 || b.Height <= 0 { | 11 | if a.Width <= 0 || a.Height <= 0 || b.Width <= 0 || b.Height <= 0 { | ||
| 28 | return false | 12 | return false | ||
| @@ -34,9 +18,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 34 | t.Parallel() | 18 | t.Parallel() | ||
| 35 | 19 | ||||
| 36 | tests := []struct { | 20 | tests := []struct { | ||
| 37 | - | name string | 21 | + | name string |
| 38 | - | root *Node | 22 | + | root *Node |
| 39 | - | area domain.Rect | 23 | + | area domain.Rect |
| 24 | + | expected []Divider | |||
| 40 | }{ | 25 | }{ | ||
| 41 | { | 26 | { | ||
| 42 | name: "equal weights", | 27 | name: "equal weights", | ||
| @@ -44,7 +29,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 44 | {Kind: Leaf, Leaf: "a", Weight: 1}, | 29 | {Kind: Leaf, Leaf: "a", Weight: 1}, | ||
| 45 | {Kind: Leaf, Leaf: "b", Weight: 1}, | 30 | {Kind: Leaf, Leaf: "b", Weight: 1}, | ||
| 46 | }}, | 31 | }}, | ||
| 47 | - | area: domain.Rect{Width: 61, Height: 10}, | 32 | + | area: domain.Rect{Width: 61, Height: 10}, |
| 33 | + | expected: []Divider{{Rect: domain.Rect{X: 30, Width: 1, Height: 10}, Dir: Horizontal}}, | |||
| 48 | }, | 34 | }, | ||
| 49 | { | 35 | { | ||
| 50 | name: "unequal weights 2:1", | 36 | name: "unequal weights 2:1", | ||
| @@ -52,7 +38,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 52 | {Kind: Leaf, Leaf: "a", Weight: 2}, | 38 | {Kind: Leaf, Leaf: "a", Weight: 2}, | ||
| 53 | {Kind: Leaf, Leaf: "b", Weight: 1}, | 39 | {Kind: Leaf, Leaf: "b", Weight: 1}, | ||
| 54 | }}, | 40 | }}, | ||
| 55 | - | area: domain.Rect{Width: 61, Height: 10}, | 41 | + | area: domain.Rect{Width: 61, Height: 10}, |
| 42 | + | expected: []Divider{{Rect: domain.Rect{X: 40, Width: 1, Height: 10}, Dir: Horizontal}}, | |||
| 56 | }, | 43 | }, | ||
| 57 | { | 44 | { | ||
| 58 | name: "unequal weights 1:3", | 45 | name: "unequal weights 1:3", | ||
| @@ -60,7 +47,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 60 | {Kind: Leaf, Leaf: "a", Weight: 1}, | 47 | {Kind: Leaf, Leaf: "a", Weight: 1}, | ||
| 61 | {Kind: Leaf, Leaf: "b", Weight: 3}, | 48 | {Kind: Leaf, Leaf: "b", Weight: 3}, | ||
| 62 | }}, | 49 | }}, | ||
| 63 | - | area: domain.Rect{Width: 81, Height: 10}, | 50 | + | area: domain.Rect{Width: 81, Height: 10}, |
| 51 | + | expected: []Divider{{Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}}, | |||
| 64 | }, | 52 | }, | ||
| 65 | { | 53 | { | ||
| 66 | name: "nested split", | 54 | name: "nested split", | ||
| @@ -72,6 +60,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 72 | }}, | 60 | }}, | ||
| 73 | }}, | 61 | }}, | ||
| 74 | area: domain.Rect{Width: 61, Height: 20}, | 62 | area: domain.Rect{Width: 61, Height: 20}, | ||
| 63 | + | expected: []Divider{ | |||
| 64 | + | {Rect: domain.Rect{X: 30, Width: 1, Height: 20}, Dir: Horizontal}, | |||
| 65 | + | {Rect: domain.Rect{X: 31, Y: 10, Width: 30, Height: 1}, Dir: Vertical}, | |||
| 66 | + | }, | |||
| 75 | }, | 67 | }, | ||
| 76 | { | 68 | { | ||
| 77 | name: "three or more children", | 69 | name: "three or more children", | ||
| @@ -81,6 +73,10 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 81 | {Kind: Leaf, Leaf: "c", Weight: 1}, | 73 | {Kind: Leaf, Leaf: "c", Weight: 1}, | ||
| 82 | }}, | 74 | }}, | ||
| 83 | area: domain.Rect{Width: 62, Height: 10}, | 75 | area: domain.Rect{Width: 62, Height: 10}, | ||
| 76 | + | expected: []Divider{ | |||
| 77 | + | {Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}, | |||
| 78 | + | {Rect: domain.Rect{X: 41, Width: 1, Height: 10}, Dir: Horizontal}, | |||
| 79 | + | }, | |||
| 84 | }, | 80 | }, | ||
| 85 | { | 81 | { | ||
| 86 | name: "minimum pins a child", | 82 | name: "minimum pins a child", | ||
| @@ -88,7 +84,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 88 | {Kind: Leaf, Leaf: "a", Weight: 1}, | 84 | {Kind: Leaf, Leaf: "a", Weight: 1}, | ||
| 89 | {Kind: Leaf, Leaf: "b", Weight: 100}, | 85 | {Kind: Leaf, Leaf: "b", Weight: 100}, | ||
| 90 | }}, | 86 | }}, | ||
| 91 | - | area: domain.Rect{Width: 61, Height: 10}, | 87 | + | area: domain.Rect{Width: 61, Height: 10}, |
| 88 | + | expected: []Divider{{Rect: domain.Rect{X: 20, Width: 1, Height: 10}, Dir: Horizontal}}, | |||
| 92 | }, | 89 | }, | ||
| 93 | { | 90 | { | ||
| 94 | name: "vertical split", | 91 | name: "vertical split", | ||
| @@ -96,7 +93,8 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 96 | {Kind: Leaf, Leaf: "a", Weight: 1}, | 93 | {Kind: Leaf, Leaf: "a", Weight: 1}, | ||
| 97 | {Kind: Leaf, Leaf: "b", Weight: 2}, | 94 | {Kind: Leaf, Leaf: "b", Weight: 2}, | ||
| 98 | }}, | 95 | }}, | ||
| 99 | - | area: domain.Rect{Width: 41, Height: 10}, | 96 | + | area: domain.Rect{Width: 41, Height: 10}, |
| 97 | + | expected: []Divider{{Rect: domain.Rect{Y: 3, Width: 41, Height: 1}, Dir: Vertical}}, | |||
| 100 | }, | 98 | }, | ||
| 101 | } | 99 | } | ||
| 102 | 100 | ||||
| @@ -110,7 +108,7 @@ func TestDividersMatchSolvedGaps(t *testing.T) { | |||||
| 110 | require.True(t, solveOK) | 108 | require.True(t, solveOK) | ||
| 111 | require.Equal(t, solved, placements, "collecting dividers must not change the solved placements") | 109 | require.Equal(t, solved, placements, "collecting dividers must not change the solved placements") | ||
| 112 | 110 | ||||
| 113 | - | require.Len(t, dividers, countGaps(tt.root), "expected exactly one divider per sibling gap") | 111 | + | require.Equal(t, tt.expected, dividers, "divider geometry and direction must match the solved gaps exactly") |
| 114 | 112 | ||||
| 115 | for _, d := range dividers { | 113 | for _, d := range dividers { | ||
| 116 | require.True(t, d.Rect.Width == 1 || d.Rect.Height == 1, "divider rect must be a single row or column") | 114 | require.True(t, d.Rect.Width == 1 || d.Rect.Height == 1, "divider rect must be a single row or column") | ||