refactor(daemon): make floating geometry content-relative
- commit
- 9dd512665ee25c7fe6e9aec313d2df8879cc4285
- author
- brice · · 2026-07-11 05:42 UTC
- parent
- d852566
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +51 -51
M internal/usecase/daemon/copymode_test.go +1 -1
| @@ -750,7 +750,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) { | |||
| 750 | 750 | d.enterCopyMode(sess, ac) | |
| 751 | 751 | awaitFrame(t, sends, ports.MsgOutput) | |
| 752 | 752 | viewportTop := ac.overlays.copyMode.ViewportTop | |
| 753 | - | inner := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 23}, d.currentFloatingConfig()).Inner | |
| 753 | + | inner := calculateContentFloatingGeometry(domain.Size{Cols: 80, Rows: 23}, d.currentFloatingConfig()).Inner | |
| 754 | 754 | ||
| 755 | 755 | // Copy body row k renders at wire row inner.Y+k+2 (top bar + content | |
| 756 | 756 | // offset); clicking a row must select exactly that row. | |
| @@ -750,7 +750,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) { | |||||
| 750 | d.enterCopyMode(sess, ac) | 750 | d.enterCopyMode(sess, ac) | ||
| 751 | awaitFrame(t, sends, ports.MsgOutput) | 751 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 752 | viewportTop := ac.overlays.copyMode.ViewportTop | 752 | viewportTop := ac.overlays.copyMode.ViewportTop | ||
| 753 | - | inner := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 23}, d.currentFloatingConfig()).Inner | 753 | + | inner := calculateContentFloatingGeometry(domain.Size{Cols: 80, Rows: 23}, d.currentFloatingConfig()).Inner |
| 754 | 754 | ||||
| 755 | // Copy body row k renders at wire row inner.Y+k+2 (top bar + content | 755 | // Copy body row k renders at wire row inner.Y+k+2 (top bar + content | ||
| 756 | // offset); clicking a row must select exactly that row. | 756 | // offset); clicking a row must select exactly that row. | ||
M internal/usecase/daemon/floating.go +2 -2
| @@ -133,7 +133,7 @@ func (tb *tab) visibleFloatingSnapshotLocked(cfg domain.FloatingConfig) (*pane, | |||
| 133 | 133 | return nil, floatingGeometry{}, false | |
| 134 | 134 | } | |
| 135 | 135 | p := tb.floating.pane | |
| 136 | - | desired := calculateFloatingGeometry(domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}, cfg) | |
| 136 | + | desired := calculateContentFloatingGeometry(domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows}, cfg) | |
| 137 | 137 | p.mu.Lock() | |
| 138 | 138 | geometry := p.committedFloatingGeometryLocked(desired) | |
| 139 | 139 | p.mu.Unlock() | |
| @@ -250,7 +250,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||
| 250 | 250 | name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx | |
| 251 | 251 | sess.mu.Unlock() | |
| 252 | 252 | tb.mu.Lock() | |
| 253 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}, cfg) | |
| 253 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows}, cfg) | |
| 254 | 254 | size := rectSize(geometry.Inner) | |
| 255 | 255 | if !size.Valid() { | |
| 256 | 256 | geometry = floatingGeometry{Bounds: domain.Rect{Width: 1, Height: 1}, Inner: domain.Rect{Width: 1, Height: 1}} | |
| @@ -133,7 +133,7 @@ func (tb *tab) visibleFloatingSnapshotLocked(cfg domain.FloatingConfig) (*pane, | |||||
| 133 | return nil, floatingGeometry{}, false | 133 | return nil, floatingGeometry{}, false | ||
| 134 | } | 134 | } | ||
| 135 | p := tb.floating.pane | 135 | p := tb.floating.pane | ||
| 136 | - | desired := calculateFloatingGeometry(domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}, cfg) | 136 | + | desired := calculateContentFloatingGeometry(domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows}, cfg) |
| 137 | p.mu.Lock() | 137 | p.mu.Lock() | ||
| 138 | geometry := p.committedFloatingGeometryLocked(desired) | 138 | geometry := p.committedFloatingGeometryLocked(desired) | ||
| 139 | p.mu.Unlock() | 139 | p.mu.Unlock() | ||
| @@ -250,7 +250,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||||
| 250 | name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx | 250 | name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx | ||
| 251 | sess.mu.Unlock() | 251 | sess.mu.Unlock() | ||
| 252 | tb.mu.Lock() | 252 | tb.mu.Lock() | ||
| 253 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}, cfg) | 253 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows}, cfg) |
| 254 | size := rectSize(geometry.Inner) | 254 | size := rectSize(geometry.Inner) | ||
| 255 | if !size.Valid() { | 255 | if !size.Valid() { | ||
| 256 | geometry = floatingGeometry{Bounds: domain.Rect{Width: 1, Height: 1}, Inner: domain.Rect{Width: 1, Height: 1}} | 256 | geometry = floatingGeometry{Bounds: domain.Rect{Width: 1, Height: 1}, Inner: domain.Rect{Width: 1, Height: 1}} | ||
M internal/usecase/daemon/floating_render.go +14 -14
| @@ -8,8 +8,8 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | // floatingGeometry describes a popup's outer frame and terminal content area. | |
| 11 | - | // Committed geometry is tab-content-relative; Bounds and Inner are both relative | |
| 12 | - | // to the supplied content rectangle. | |
| 11 | + | // Committed geometry is tab-content-relative and origin-zero; Bounds and Inner | |
| 12 | + | // are both relative to tab content. | |
| 13 | 13 | type floatingGeometry struct { | |
| 14 | 14 | Bounds domain.Rect | |
| 15 | 15 | Inner domain.Rect | |
| @@ -56,19 +56,19 @@ func calculateFloatingAxisGeometry(available, percent int) floatingAxisGeometry | |||
| 56 | 56 | return axis | |
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | - | // calculateFloatingGeometry returns coordinates in the supplied rectangle's | |
| 60 | - | // space and centers a percentage-sized popup in content. Launch sizing derives | |
| 61 | - | // from Inner too, so rendering and PTY geometry share | |
| 62 | - | // the same per-axis percentage and tiny-border rules. | |
| 63 | - | func calculateFloatingGeometry(content domain.Rect, cfg domain.FloatingConfig) floatingGeometry { | |
| 64 | - | if content.Width <= 0 || content.Height <= 0 { | |
| 59 | + | // calculateContentFloatingGeometry returns origin-zero, tab-content-relative | |
| 60 | + | // coordinates for a percentage-sized popup. Launch sizing derives from Inner | |
| 61 | + | // too, so rendering and PTY geometry share the same per-axis percentage and | |
| 62 | + | // tiny-border rules. | |
| 63 | + | func calculateContentFloatingGeometry(content domain.Size, cfg domain.FloatingConfig) floatingGeometry { | |
| 64 | + | if !content.Valid() { | |
| 65 | 65 | return floatingGeometry{} | |
| 66 | 66 | } | |
| 67 | - | x := calculateFloatingAxisGeometry(content.Width, cfg.Width) | |
| 68 | - | y := calculateFloatingAxisGeometry(content.Height, cfg.Height) | |
| 67 | + | x := calculateFloatingAxisGeometry(content.Cols, cfg.Width) | |
| 68 | + | y := calculateFloatingAxisGeometry(content.Rows, cfg.Height) | |
| 69 | 69 | bounds := domain.Rect{ | |
| 70 | - | X: content.X + (content.Width-x.BoundsSize)/2, | |
| 71 | - | Y: content.Y + (content.Height-y.BoundsSize)/2, | |
| 70 | + | X: (content.Cols - x.BoundsSize) / 2, | |
| 71 | + | Y: (content.Rows - y.BoundsSize) / 2, | |
| 72 | 72 | Width: x.BoundsSize, | |
| 73 | 73 | Height: y.BoundsSize, | |
| 74 | 74 | } | |
| @@ -211,12 +211,12 @@ func (d *Daemon) resizeInstalledFloating(tb *tab) bool { | |||
| 211 | 211 | } | |
| 212 | 212 | tb.mu.Lock() | |
| 213 | 213 | p := tb.floating.pane | |
| 214 | - | content := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | |
| 214 | + | content := domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows} | |
| 215 | 215 | tb.mu.Unlock() | |
| 216 | 216 | if p == nil { | |
| 217 | 217 | return false | |
| 218 | 218 | } | |
| 219 | - | return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig())) | |
| 219 | + | return d.resizeFloatingPane(p, calculateContentFloatingGeometry(content, d.currentFloatingConfig())) | |
| 220 | 220 | } | |
| 221 | 221 | ||
| 222 | 222 | // resizeActiveFloating updates a visible floating pane during a client resize. | |
| @@ -8,8 +8,8 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | // floatingGeometry describes a popup's outer frame and terminal content area. | 10 | // floatingGeometry describes a popup's outer frame and terminal content area. | ||
| 11 | - | // Committed geometry is tab-content-relative; Bounds and Inner are both relative | 11 | + | // Committed geometry is tab-content-relative and origin-zero; Bounds and Inner |
| 12 | - | // to the supplied content rectangle. | 12 | + | // are both relative to tab content. |
| 13 | type floatingGeometry struct { | 13 | type floatingGeometry struct { | ||
| 14 | Bounds domain.Rect | 14 | Bounds domain.Rect | ||
| 15 | Inner domain.Rect | 15 | Inner domain.Rect | ||
| @@ -56,19 +56,19 @@ func calculateFloatingAxisGeometry(available, percent int) floatingAxisGeometry | |||||
| 56 | return axis | 56 | return axis | ||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | - | // calculateFloatingGeometry returns coordinates in the supplied rectangle's | 59 | + | // calculateContentFloatingGeometry returns origin-zero, tab-content-relative |
| 60 | - | // space and centers a percentage-sized popup in content. Launch sizing derives | 60 | + | // coordinates for a percentage-sized popup. Launch sizing derives from Inner |
| 61 | - | // from Inner too, so rendering and PTY geometry share | 61 | + | // too, so rendering and PTY geometry share the same per-axis percentage and |
| 62 | - | // the same per-axis percentage and tiny-border rules. | 62 | + | // tiny-border rules. |
| 63 | - | func calculateFloatingGeometry(content domain.Rect, cfg domain.FloatingConfig) floatingGeometry { | 63 | + | func calculateContentFloatingGeometry(content domain.Size, cfg domain.FloatingConfig) floatingGeometry { |
| 64 | - | if content.Width <= 0 || content.Height <= 0 { | 64 | + | if !content.Valid() { |
| 65 | return floatingGeometry{} | 65 | return floatingGeometry{} | ||
| 66 | } | 66 | } | ||
| 67 | - | x := calculateFloatingAxisGeometry(content.Width, cfg.Width) | 67 | + | x := calculateFloatingAxisGeometry(content.Cols, cfg.Width) |
| 68 | - | y := calculateFloatingAxisGeometry(content.Height, cfg.Height) | 68 | + | y := calculateFloatingAxisGeometry(content.Rows, cfg.Height) |
| 69 | bounds := domain.Rect{ | 69 | bounds := domain.Rect{ | ||
| 70 | - | X: content.X + (content.Width-x.BoundsSize)/2, | 70 | + | X: (content.Cols - x.BoundsSize) / 2, |
| 71 | - | Y: content.Y + (content.Height-y.BoundsSize)/2, | 71 | + | Y: (content.Rows - y.BoundsSize) / 2, |
| 72 | Width: x.BoundsSize, | 72 | Width: x.BoundsSize, | ||
| 73 | Height: y.BoundsSize, | 73 | Height: y.BoundsSize, | ||
| 74 | } | 74 | } | ||
| @@ -211,12 +211,12 @@ func (d *Daemon) resizeInstalledFloating(tb *tab) bool { | |||||
| 211 | } | 211 | } | ||
| 212 | tb.mu.Lock() | 212 | tb.mu.Lock() | ||
| 213 | p := tb.floating.pane | 213 | p := tb.floating.pane | ||
| 214 | - | content := domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows} | 214 | + | content := domain.Size{Cols: tb.size.Cols, Rows: tb.size.Rows} |
| 215 | tb.mu.Unlock() | 215 | tb.mu.Unlock() | ||
| 216 | if p == nil { | 216 | if p == nil { | ||
| 217 | return false | 217 | return false | ||
| 218 | } | 218 | } | ||
| 219 | - | return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig())) | 219 | + | return d.resizeFloatingPane(p, calculateContentFloatingGeometry(content, d.currentFloatingConfig())) |
| 220 | } | 220 | } | ||
| 221 | 221 | ||||
| 222 | // resizeActiveFloating updates a visible floating pane during a client resize. | 222 | // resizeActiveFloating updates a visible floating pane during a client resize. | ||
M internal/usecase/daemon/floating_render_test.go +28 -28
| @@ -35,18 +35,18 @@ func TestFloatingFrameGeometryPreservesBorderRules(t *testing.T) { | |||
| 35 | 35 | const frameX, frameY = 7, 3 | |
| 36 | 36 | tests := []struct { | |
| 37 | 37 | name string | |
| 38 | - | content domain.Rect | |
| 38 | + | content domain.Size | |
| 39 | 39 | cfg domain.FloatingConfig | |
| 40 | 40 | tinyWidth, tinyHeight bool | |
| 41 | 41 | }{ | |
| 42 | - | {name: "normal", content: domain.Rect{Width: 80, Height: 24}, cfg: domain.FloatingConfig{Width: 80, Height: 75}}, | |
| 43 | - | {name: "tiny width omits horizontal borders", content: domain.Rect{Width: 2, Height: 20}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyWidth: true}, | |
| 44 | - | {name: "tiny height omits vertical borders", content: domain.Rect{Width: 20, Height: 2}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyHeight: true}, | |
| 45 | - | {name: "full size", content: domain.Rect{Width: 80, Height: 24}, cfg: domain.FloatingConfig{Width: 100, Height: 100}}, | |
| 42 | + | {name: "normal", content: domain.Size{Cols: 80, Rows: 24}, cfg: domain.FloatingConfig{Width: 80, Height: 75}}, | |
| 43 | + | {name: "tiny width omits horizontal borders", content: domain.Size{Cols: 2, Rows: 20}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyWidth: true}, | |
| 44 | + | {name: "tiny height omits vertical borders", content: domain.Size{Cols: 20, Rows: 2}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyHeight: true}, | |
| 45 | + | {name: "full size", content: domain.Size{Cols: 80, Rows: 24}, cfg: domain.FloatingConfig{Width: 100, Height: 100}}, | |
| 46 | 46 | } | |
| 47 | 47 | for _, tt := range tests { | |
| 48 | 48 | t.Run(tt.name, func(t *testing.T) { | |
| 49 | - | contentGeometry := calculateFloatingGeometry(tt.content, tt.cfg) | |
| 49 | + | contentGeometry := calculateContentFloatingGeometry(tt.content, tt.cfg) | |
| 50 | 50 | frameGeometry := contentGeometry.translate(frameX, frameY) | |
| 51 | 51 | ||
| 52 | 52 | require.Equal(t, contentGeometry.Bounds.X+frameX, frameGeometry.Bounds.X) | |
| @@ -73,19 +73,19 @@ func TestFloatingFrameGeometryPreservesBorderRules(t *testing.T) { | |||
| 73 | 73 | func TestCalculateFloatingGeometry(t *testing.T) { | |
| 74 | 74 | tests := []struct { | |
| 75 | 75 | name string | |
| 76 | - | content domain.Rect | |
| 76 | + | content domain.Size | |
| 77 | 77 | cfg domain.FloatingConfig | |
| 78 | 78 | want floatingGeometry | |
| 79 | 79 | }{ | |
| 80 | - | {"eighty percent centered", domain.Rect{X: 3, Y: 1, Width: 101, Height: 51}, domain.FloatingConfig{Width: 80, Height: 80}, floatingGeometry{Bounds: domain.Rect{X: 13, Y: 6, Width: 80, Height: 40}, Inner: domain.Rect{X: 14, Y: 7, Width: 78, Height: 38}}}, | |
| 81 | - | {"one percent clamps to one", domain.Rect{Y: 1, Width: 100, Height: 20}, domain.FloatingConfig{Width: 1, Height: 1}, floatingGeometry{Bounds: domain.Rect{X: 49, Y: 10, Width: 1, Height: 1}, Inner: domain.Rect{X: 49, Y: 10, Width: 1, Height: 1}}}, | |
| 82 | - | {"full size", domain.Rect{Y: 1, Width: 100, Height: 20}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Y: 1, Width: 100, Height: 20}, Inner: domain.Rect{X: 1, Y: 2, Width: 98, Height: 18}}}, | |
| 83 | - | {"tiny axes omit borders", domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}, Inner: domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}}}, | |
| 84 | - | {"percent clamps", domain.Rect{Width: 10, Height: 10}, domain.FloatingConfig{Width: 101, Height: -1}, floatingGeometry{Bounds: domain.Rect{Width: 10, Height: 1, Y: 4}, Inner: domain.Rect{X: 1, Width: 8, Y: 4, Height: 1}}}, | |
| 80 | + | {"eighty percent centered", domain.Size{Cols: 101, Rows: 51}, domain.FloatingConfig{Width: 80, Height: 80}, floatingGeometry{Bounds: domain.Rect{X: 10, Y: 5, Width: 80, Height: 40}, Inner: domain.Rect{X: 11, Y: 6, Width: 78, Height: 38}}}, | |
| 81 | + | {"one percent clamps to one", domain.Size{Cols: 100, Rows: 20}, domain.FloatingConfig{Width: 1, Height: 1}, floatingGeometry{Bounds: domain.Rect{X: 49, Y: 9, Width: 1, Height: 1}, Inner: domain.Rect{X: 49, Y: 9, Width: 1, Height: 1}}}, | |
| 82 | + | {"full size", domain.Size{Cols: 100, Rows: 20}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Width: 100, Height: 20}, Inner: domain.Rect{X: 1, Y: 1, Width: 98, Height: 18}}}, | |
| 83 | + | {"tiny axes omit borders", domain.Size{Cols: 2, Rows: 1}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Width: 2, Height: 1}, Inner: domain.Rect{Width: 2, Height: 1}}}, | |
| 84 | + | {"percent clamps", domain.Size{Cols: 10, Rows: 10}, domain.FloatingConfig{Width: 101, Height: -1}, floatingGeometry{Bounds: domain.Rect{Width: 10, Height: 1, Y: 4}, Inner: domain.Rect{X: 1, Width: 8, Y: 4, Height: 1}}}, | |
| 85 | 85 | } | |
| 86 | 86 | for _, tt := range tests { | |
| 87 | 87 | t.Run(tt.name, func(t *testing.T) { | |
| 88 | - | require.Equal(t, tt.want, calculateFloatingGeometry(tt.content, tt.cfg)) | |
| 88 | + | require.Equal(t, tt.want, calculateContentFloatingGeometry(tt.content, tt.cfg)) | |
| 89 | 89 | }) | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| @@ -117,9 +117,9 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) { | |||
| 117 | 117 | base.Set(2, 2, renderer.Cell{Rune: 'B'}) | |
| 118 | 118 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | |
| 119 | 119 | cache := &composedFrameCache{} | |
| 120 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 120 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 121 | 121 | require.Equal(t, 'B', base.At(2, 2).Rune, "backdrop must only touch the composed destination") | |
| 122 | - | geometry := calculateFloatingGeometry(content, domain.FloatingConfig{Width: 80, Height: 80}) | |
| 122 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}).translate(content.X, content.Y) | |
| 123 | 123 | require.Equal(t, '┌', frame.At(geometry.Bounds.X, geometry.Bounds.Y).Rune) | |
| 124 | 124 | require.Equal(t, 'F', frame.At(geometry.Inner.X, geometry.Inner.Y).Rune) | |
| 125 | 125 | var gotTitle strings.Builder | |
| @@ -131,7 +131,7 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) { | |||
| 131 | 131 | p.mu.Lock() | |
| 132 | 132 | p.title.generation++ | |
| 133 | 133 | p.mu.Unlock() | |
| 134 | - | _, damage, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 134 | + | _, damage, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 135 | 135 | require.Len(t, damage, 1) | |
| 136 | 136 | require.Equal(t, geometry.Bounds.Y, damage[0].Y) | |
| 137 | 137 | require.Equal(t, 1, damage[0].Height) | |
| @@ -220,8 +220,8 @@ func TestComposeFloatingFrameRendersPaneOwnedCommandFallback(t *testing.T) { | |||
| 220 | 220 | base := renderer.NewFrame(40, 12) | |
| 221 | 221 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | |
| 222 | 222 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | |
| 223 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, false) | |
| 224 | - | geometry := calculateFloatingGeometry(content, cfg) | |
| 223 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, false) | |
| 224 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg).translate(content.X, content.Y) | |
| 225 | 225 | var gotTitle strings.Builder | |
| 226 | 226 | for x := geometry.Bounds.X + 2; x < geometry.Bounds.X+geometry.Bounds.Width-2; x++ { | |
| 227 | 227 | gotTitle.WriteRune(frame.At(x, geometry.Bounds.Y).Rune) | |
| @@ -253,7 +253,7 @@ func TestComposeFloatingFrameSynchronizesWithPTYReader(t *testing.T) { | |||
| 253 | 253 | <-start | |
| 254 | 254 | cache := &composedFrameCache{} | |
| 255 | 255 | for range 500 { | |
| 256 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 256 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 257 | 257 | } | |
| 258 | 258 | }() | |
| 259 | 259 | close(start) | |
| @@ -310,13 +310,13 @@ func BenchmarkComposeFloatingFrameCached(b *testing.B) { | |||
| 310 | 310 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | |
| 311 | 311 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | |
| 312 | 312 | cache := &composedFrameCache{} | |
| 313 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 313 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 314 | 314 | p.screen.ClearDamage() | |
| 315 | 315 | ||
| 316 | 316 | b.ReportAllocs() | |
| 317 | 317 | b.ResetTimer() | |
| 318 | 318 | for b.Loop() { | |
| 319 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 319 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 320 | 320 | } | |
| 321 | 321 | } | |
| 322 | 322 | ||
| @@ -327,11 +327,11 @@ func TestComposeFloatingFrameCachedDoesNotAllocate(t *testing.T) { | |||
| 327 | 327 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | |
| 328 | 328 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | |
| 329 | 329 | cache := &composedFrameCache{} | |
| 330 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 330 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 331 | 331 | p.screen.ClearDamage() | |
| 332 | 332 | ||
| 333 | 333 | allocs := testing.AllocsPerRun(100, func() { | |
| 334 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 334 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | |
| 335 | 335 | }) | |
| 336 | 336 | require.Zero(t, allocs, "cached floating composition must not allocate without damage or title changes") | |
| 337 | 337 | } | |
| @@ -342,8 +342,8 @@ func TestToggleFloatingResizesHiddenPaneOnShowAndRetriesFailure(t *testing.T) { | |||
| 342 | 342 | tb := newTab(nil, domain.Size{Cols: 80, Rows: 24}) | |
| 343 | 343 | sess := &session{tabs: []*tab{tb}} | |
| 344 | 344 | ||
| 345 | - | initialGeometry := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 24}, d.currentFloatingConfig()) | |
| 346 | - | currentGeometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 40}, d.currentFloatingConfig()) | |
| 345 | + | initialGeometry := calculateContentFloatingGeometry(domain.Size{Cols: 80, Rows: 24}, d.currentFloatingConfig()) | |
| 346 | + | currentGeometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 40}, d.currentFloatingConfig()) | |
| 347 | 347 | initial := initialGeometry.Inner | |
| 348 | 348 | current := currentGeometry.Inner | |
| 349 | 349 | pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}} | |
| @@ -384,8 +384,8 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T) | |||
| 384 | 384 | cfg := domain.FloatingConfig{Width: 50, Height: 50} | |
| 385 | 385 | oldContent := domain.Rect{Y: 1, Width: 80, Height: 24} | |
| 386 | 386 | newContent := domain.Rect{Y: 1, Width: 100, Height: 40} | |
| 387 | - | oldGeometry := calculateFloatingGeometry(domain.Rect{Width: oldContent.Width, Height: oldContent.Height}, cfg) | |
| 388 | - | newGeometry := calculateFloatingGeometry(domain.Rect{Width: newContent.Width, Height: newContent.Height}, cfg) | |
| 387 | + | oldGeometry := calculateContentFloatingGeometry(domain.Size{Cols: oldContent.Width, Rows: oldContent.Height}, cfg) | |
| 388 | + | newGeometry := calculateContentFloatingGeometry(domain.Size{Cols: newContent.Width, Rows: newContent.Height}, cfg) | |
| 389 | 389 | pty := &resizePTY{err: errors.New("resize failed")} | |
| 390 | 390 | p := newPane("floating", pty, rectSize(oldGeometry.Inner)) | |
| 391 | 391 | p.rect = oldGeometry.Inner | |
| @@ -405,7 +405,7 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T) | |||
| 405 | 405 | require.Equal(t, oldGeometry.translate(newContent.X, newContent.Y).Inner, copyRect) | |
| 406 | 406 | ||
| 407 | 407 | base := renderer.NewFrame(newContent.Width, newContent.Height+2) | |
| 408 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, newContent, calculateFloatingGeometry(domain.Rect{Width: newContent.Width, Height: newContent.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, true) | |
| 408 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, newContent, calculateContentFloatingGeometry(domain.Size{Cols: newContent.Width, Rows: newContent.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, true) | |
| 409 | 409 | oldFrameGeometry := oldGeometry.translate(newContent.X, newContent.Y) | |
| 410 | 410 | newFrameGeometry := newGeometry.translate(newContent.X, newContent.Y) | |
| 411 | 411 | require.Equal(t, '┌', frame.At(oldFrameGeometry.Bounds.X, oldFrameGeometry.Bounds.Y).Rune) | |
| @@ -35,18 +35,18 @@ func TestFloatingFrameGeometryPreservesBorderRules(t *testing.T) { | |||||
| 35 | const frameX, frameY = 7, 3 | 35 | const frameX, frameY = 7, 3 | ||
| 36 | tests := []struct { | 36 | tests := []struct { | ||
| 37 | name string | 37 | name string | ||
| 38 | - | content domain.Rect | 38 | + | content domain.Size |
| 39 | cfg domain.FloatingConfig | 39 | cfg domain.FloatingConfig | ||
| 40 | tinyWidth, tinyHeight bool | 40 | tinyWidth, tinyHeight bool | ||
| 41 | }{ | 41 | }{ | ||
| 42 | - | {name: "normal", content: domain.Rect{Width: 80, Height: 24}, cfg: domain.FloatingConfig{Width: 80, Height: 75}}, | 42 | + | {name: "normal", content: domain.Size{Cols: 80, Rows: 24}, cfg: domain.FloatingConfig{Width: 80, Height: 75}}, |
| 43 | - | {name: "tiny width omits horizontal borders", content: domain.Rect{Width: 2, Height: 20}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyWidth: true}, | 43 | + | {name: "tiny width omits horizontal borders", content: domain.Size{Cols: 2, Rows: 20}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyWidth: true}, |
| 44 | - | {name: "tiny height omits vertical borders", content: domain.Rect{Width: 20, Height: 2}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyHeight: true}, | 44 | + | {name: "tiny height omits vertical borders", content: domain.Size{Cols: 20, Rows: 2}, cfg: domain.FloatingConfig{Width: 100, Height: 100}, tinyHeight: true}, |
| 45 | - | {name: "full size", content: domain.Rect{Width: 80, Height: 24}, cfg: domain.FloatingConfig{Width: 100, Height: 100}}, | 45 | + | {name: "full size", content: domain.Size{Cols: 80, Rows: 24}, cfg: domain.FloatingConfig{Width: 100, Height: 100}}, |
| 46 | } | 46 | } | ||
| 47 | for _, tt := range tests { | 47 | for _, tt := range tests { | ||
| 48 | t.Run(tt.name, func(t *testing.T) { | 48 | t.Run(tt.name, func(t *testing.T) { | ||
| 49 | - | contentGeometry := calculateFloatingGeometry(tt.content, tt.cfg) | 49 | + | contentGeometry := calculateContentFloatingGeometry(tt.content, tt.cfg) |
| 50 | frameGeometry := contentGeometry.translate(frameX, frameY) | 50 | frameGeometry := contentGeometry.translate(frameX, frameY) | ||
| 51 | 51 | ||||
| 52 | require.Equal(t, contentGeometry.Bounds.X+frameX, frameGeometry.Bounds.X) | 52 | require.Equal(t, contentGeometry.Bounds.X+frameX, frameGeometry.Bounds.X) | ||
| @@ -73,19 +73,19 @@ func TestFloatingFrameGeometryPreservesBorderRules(t *testing.T) { | |||||
| 73 | func TestCalculateFloatingGeometry(t *testing.T) { | 73 | func TestCalculateFloatingGeometry(t *testing.T) { | ||
| 74 | tests := []struct { | 74 | tests := []struct { | ||
| 75 | name string | 75 | name string | ||
| 76 | - | content domain.Rect | 76 | + | content domain.Size |
| 77 | cfg domain.FloatingConfig | 77 | cfg domain.FloatingConfig | ||
| 78 | want floatingGeometry | 78 | want floatingGeometry | ||
| 79 | }{ | 79 | }{ | ||
| 80 | - | {"eighty percent centered", domain.Rect{X: 3, Y: 1, Width: 101, Height: 51}, domain.FloatingConfig{Width: 80, Height: 80}, floatingGeometry{Bounds: domain.Rect{X: 13, Y: 6, Width: 80, Height: 40}, Inner: domain.Rect{X: 14, Y: 7, Width: 78, Height: 38}}}, | 80 | + | {"eighty percent centered", domain.Size{Cols: 101, Rows: 51}, domain.FloatingConfig{Width: 80, Height: 80}, floatingGeometry{Bounds: domain.Rect{X: 10, Y: 5, Width: 80, Height: 40}, Inner: domain.Rect{X: 11, Y: 6, Width: 78, Height: 38}}}, |
| 81 | - | {"one percent clamps to one", domain.Rect{Y: 1, Width: 100, Height: 20}, domain.FloatingConfig{Width: 1, Height: 1}, floatingGeometry{Bounds: domain.Rect{X: 49, Y: 10, Width: 1, Height: 1}, Inner: domain.Rect{X: 49, Y: 10, Width: 1, Height: 1}}}, | 81 | + | {"one percent clamps to one", domain.Size{Cols: 100, Rows: 20}, domain.FloatingConfig{Width: 1, Height: 1}, floatingGeometry{Bounds: domain.Rect{X: 49, Y: 9, Width: 1, Height: 1}, Inner: domain.Rect{X: 49, Y: 9, Width: 1, Height: 1}}}, |
| 82 | - | {"full size", domain.Rect{Y: 1, Width: 100, Height: 20}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Y: 1, Width: 100, Height: 20}, Inner: domain.Rect{X: 1, Y: 2, Width: 98, Height: 18}}}, | 82 | + | {"full size", domain.Size{Cols: 100, Rows: 20}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Width: 100, Height: 20}, Inner: domain.Rect{X: 1, Y: 1, Width: 98, Height: 18}}}, |
| 83 | - | {"tiny axes omit borders", domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}, Inner: domain.Rect{X: 2, Y: 1, Width: 2, Height: 1}}}, | 83 | + | {"tiny axes omit borders", domain.Size{Cols: 2, Rows: 1}, domain.FloatingConfig{Width: 100, Height: 100}, floatingGeometry{Bounds: domain.Rect{Width: 2, Height: 1}, Inner: domain.Rect{Width: 2, Height: 1}}}, |
| 84 | - | {"percent clamps", domain.Rect{Width: 10, Height: 10}, domain.FloatingConfig{Width: 101, Height: -1}, floatingGeometry{Bounds: domain.Rect{Width: 10, Height: 1, Y: 4}, Inner: domain.Rect{X: 1, Width: 8, Y: 4, Height: 1}}}, | 84 | + | {"percent clamps", domain.Size{Cols: 10, Rows: 10}, domain.FloatingConfig{Width: 101, Height: -1}, floatingGeometry{Bounds: domain.Rect{Width: 10, Height: 1, Y: 4}, Inner: domain.Rect{X: 1, Width: 8, Y: 4, Height: 1}}}, |
| 85 | } | 85 | } | ||
| 86 | for _, tt := range tests { | 86 | for _, tt := range tests { | ||
| 87 | t.Run(tt.name, func(t *testing.T) { | 87 | t.Run(tt.name, func(t *testing.T) { | ||
| 88 | - | require.Equal(t, tt.want, calculateFloatingGeometry(tt.content, tt.cfg)) | 88 | + | require.Equal(t, tt.want, calculateContentFloatingGeometry(tt.content, tt.cfg)) |
| 89 | }) | 89 | }) | ||
| 90 | } | 90 | } | ||
| 91 | } | 91 | } | ||
| @@ -117,9 +117,9 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) { | |||||
| 117 | base.Set(2, 2, renderer.Cell{Rune: 'B'}) | 117 | base.Set(2, 2, renderer.Cell{Rune: 'B'}) | ||
| 118 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | 118 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | ||
| 119 | cache := &composedFrameCache{} | 119 | cache := &composedFrameCache{} | ||
| 120 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 120 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 121 | require.Equal(t, 'B', base.At(2, 2).Rune, "backdrop must only touch the composed destination") | 121 | require.Equal(t, 'B', base.At(2, 2).Rune, "backdrop must only touch the composed destination") | ||
| 122 | - | geometry := calculateFloatingGeometry(content, domain.FloatingConfig{Width: 80, Height: 80}) | 122 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}).translate(content.X, content.Y) |
| 123 | require.Equal(t, '┌', frame.At(geometry.Bounds.X, geometry.Bounds.Y).Rune) | 123 | require.Equal(t, '┌', frame.At(geometry.Bounds.X, geometry.Bounds.Y).Rune) | ||
| 124 | require.Equal(t, 'F', frame.At(geometry.Inner.X, geometry.Inner.Y).Rune) | 124 | require.Equal(t, 'F', frame.At(geometry.Inner.X, geometry.Inner.Y).Rune) | ||
| 125 | var gotTitle strings.Builder | 125 | var gotTitle strings.Builder | ||
| @@ -131,7 +131,7 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) { | |||||
| 131 | p.mu.Lock() | 131 | p.mu.Lock() | ||
| 132 | p.title.generation++ | 132 | p.title.generation++ | ||
| 133 | p.mu.Unlock() | 133 | p.mu.Unlock() | ||
| 134 | - | _, damage, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 134 | + | _, damage, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, domain.FloatingConfig{Width: 80, Height: 80}), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 135 | require.Len(t, damage, 1) | 135 | require.Len(t, damage, 1) | ||
| 136 | require.Equal(t, geometry.Bounds.Y, damage[0].Y) | 136 | require.Equal(t, geometry.Bounds.Y, damage[0].Y) | ||
| 137 | require.Equal(t, 1, damage[0].Height) | 137 | require.Equal(t, 1, damage[0].Height) | ||
| @@ -220,8 +220,8 @@ func TestComposeFloatingFrameRendersPaneOwnedCommandFallback(t *testing.T) { | |||||
| 220 | base := renderer.NewFrame(40, 12) | 220 | base := renderer.NewFrame(40, 12) | ||
| 221 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | 221 | content := domain.Rect{Y: 1, Width: 40, Height: 10} | ||
| 222 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | 222 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | ||
| 223 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, false) | 223 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, false) |
| 224 | - | geometry := calculateFloatingGeometry(content, cfg) | 224 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg).translate(content.X, content.Y) |
| 225 | var gotTitle strings.Builder | 225 | var gotTitle strings.Builder | ||
| 226 | for x := geometry.Bounds.X + 2; x < geometry.Bounds.X+geometry.Bounds.Width-2; x++ { | 226 | for x := geometry.Bounds.X + 2; x < geometry.Bounds.X+geometry.Bounds.Width-2; x++ { | ||
| 227 | gotTitle.WriteRune(frame.At(x, geometry.Bounds.Y).Rune) | 227 | gotTitle.WriteRune(frame.At(x, geometry.Bounds.Y).Rune) | ||
| @@ -253,7 +253,7 @@ func TestComposeFloatingFrameSynchronizesWithPTYReader(t *testing.T) { | |||||
| 253 | <-start | 253 | <-start | ||
| 254 | cache := &composedFrameCache{} | 254 | cache := &composedFrameCache{} | ||
| 255 | for range 500 { | 255 | for range 500 { | ||
| 256 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 256 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 257 | } | 257 | } | ||
| 258 | }() | 258 | }() | ||
| 259 | close(start) | 259 | close(start) | ||
| @@ -310,13 +310,13 @@ func BenchmarkComposeFloatingFrameCached(b *testing.B) { | |||||
| 310 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | 310 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | ||
| 311 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | 311 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | ||
| 312 | cache := &composedFrameCache{} | 312 | cache := &composedFrameCache{} | ||
| 313 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 313 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 314 | p.screen.ClearDamage() | 314 | p.screen.ClearDamage() | ||
| 315 | 315 | ||||
| 316 | b.ReportAllocs() | 316 | b.ReportAllocs() | ||
| 317 | b.ResetTimer() | 317 | b.ResetTimer() | ||
| 318 | for b.Loop() { | 318 | for b.Loop() { | ||
| 319 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 319 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 320 | } | 320 | } | ||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| @@ -327,11 +327,11 @@ func TestComposeFloatingFrameCachedDoesNotAllocate(t *testing.T) { | |||||
| 327 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | 327 | content := domain.Rect{Y: 1, Width: 80, Height: 22} | ||
| 328 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | 328 | cfg := domain.FloatingConfig{Width: 80, Height: 80} | ||
| 329 | cache := &composedFrameCache{} | 329 | cache := &composedFrameCache{} | ||
| 330 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 330 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 331 | p.screen.ClearDamage() | 331 | p.screen.ClearDamage() | ||
| 332 | 332 | ||||
| 333 | allocs := testing.AllocsPerRun(100, func() { | 333 | allocs := testing.AllocsPerRun(100, func() { | ||
| 334 | - | composeFloatingFrame(base, nil, p, 1, content, calculateFloatingGeometry(domain.Rect{Width: content.Width, Height: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) | 334 | + | composeFloatingFrame(base, nil, p, 1, content, calculateContentFloatingGeometry(domain.Size{Cols: content.Width, Rows: content.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, cache, false) |
| 335 | }) | 335 | }) | ||
| 336 | require.Zero(t, allocs, "cached floating composition must not allocate without damage or title changes") | 336 | require.Zero(t, allocs, "cached floating composition must not allocate without damage or title changes") | ||
| 337 | } | 337 | } | ||
| @@ -342,8 +342,8 @@ func TestToggleFloatingResizesHiddenPaneOnShowAndRetriesFailure(t *testing.T) { | |||||
| 342 | tb := newTab(nil, domain.Size{Cols: 80, Rows: 24}) | 342 | tb := newTab(nil, domain.Size{Cols: 80, Rows: 24}) | ||
| 343 | sess := &session{tabs: []*tab{tb}} | 343 | sess := &session{tabs: []*tab{tb}} | ||
| 344 | 344 | ||||
| 345 | - | initialGeometry := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 24}, d.currentFloatingConfig()) | 345 | + | initialGeometry := calculateContentFloatingGeometry(domain.Size{Cols: 80, Rows: 24}, d.currentFloatingConfig()) |
| 346 | - | currentGeometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 40}, d.currentFloatingConfig()) | 346 | + | currentGeometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 40}, d.currentFloatingConfig()) |
| 347 | initial := initialGeometry.Inner | 347 | initial := initialGeometry.Inner | ||
| 348 | current := currentGeometry.Inner | 348 | current := currentGeometry.Inner | ||
| 349 | pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}} | 349 | pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}} | ||
| @@ -384,8 +384,8 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T) | |||||
| 384 | cfg := domain.FloatingConfig{Width: 50, Height: 50} | 384 | cfg := domain.FloatingConfig{Width: 50, Height: 50} | ||
| 385 | oldContent := domain.Rect{Y: 1, Width: 80, Height: 24} | 385 | oldContent := domain.Rect{Y: 1, Width: 80, Height: 24} | ||
| 386 | newContent := domain.Rect{Y: 1, Width: 100, Height: 40} | 386 | newContent := domain.Rect{Y: 1, Width: 100, Height: 40} | ||
| 387 | - | oldGeometry := calculateFloatingGeometry(domain.Rect{Width: oldContent.Width, Height: oldContent.Height}, cfg) | 387 | + | oldGeometry := calculateContentFloatingGeometry(domain.Size{Cols: oldContent.Width, Rows: oldContent.Height}, cfg) |
| 388 | - | newGeometry := calculateFloatingGeometry(domain.Rect{Width: newContent.Width, Height: newContent.Height}, cfg) | 388 | + | newGeometry := calculateContentFloatingGeometry(domain.Size{Cols: newContent.Width, Rows: newContent.Height}, cfg) |
| 389 | pty := &resizePTY{err: errors.New("resize failed")} | 389 | pty := &resizePTY{err: errors.New("resize failed")} | ||
| 390 | p := newPane("floating", pty, rectSize(oldGeometry.Inner)) | 390 | p := newPane("floating", pty, rectSize(oldGeometry.Inner)) | ||
| 391 | p.rect = oldGeometry.Inner | 391 | p.rect = oldGeometry.Inner | ||
| @@ -405,7 +405,7 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T) | |||||
| 405 | require.Equal(t, oldGeometry.translate(newContent.X, newContent.Y).Inner, copyRect) | 405 | require.Equal(t, oldGeometry.translate(newContent.X, newContent.Y).Inner, copyRect) | ||
| 406 | 406 | ||||
| 407 | base := renderer.NewFrame(newContent.Width, newContent.Height+2) | 407 | base := renderer.NewFrame(newContent.Width, newContent.Height+2) | ||
| 408 | - | frame, _, _ := composeFloatingFrame(base, nil, p, 1, newContent, calculateFloatingGeometry(domain.Rect{Width: newContent.Width, Height: newContent.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, true) | 408 | + | frame, _, _ := composeFloatingFrame(base, nil, p, 1, newContent, calculateContentFloatingGeometry(domain.Size{Cols: newContent.Width, Rows: newContent.Height}, cfg), tabLayoutSnapshot{}, themeui.Theme{}, &composedFrameCache{}, true) |
| 409 | oldFrameGeometry := oldGeometry.translate(newContent.X, newContent.Y) | 409 | oldFrameGeometry := oldGeometry.translate(newContent.X, newContent.Y) | ||
| 410 | newFrameGeometry := newGeometry.translate(newContent.X, newContent.Y) | 410 | newFrameGeometry := newGeometry.translate(newContent.X, newContent.Y) | ||
| 411 | require.Equal(t, '┌', frame.At(oldFrameGeometry.Bounds.X, oldFrameGeometry.Bounds.Y).Rune) | 411 | require.Equal(t, '┌', frame.At(oldFrameGeometry.Bounds.X, oldFrameGeometry.Bounds.Y).Rune) | ||
M internal/usecase/daemon/input_test.go +2 -2
| @@ -223,7 +223,7 @@ func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) { | |||
| 223 | 223 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | |
| 224 | 224 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | |
| 225 | 225 | installTestFloating(tb, floating, true) | |
| 226 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 20}, d.currentFloatingConfig()) | |
| 226 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 20}, d.currentFloatingConfig()) | |
| 227 | 227 | raw := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Inner.X+1, geometry.Inner.Y+2)) | |
| 228 | 228 | ||
| 229 | 229 | d.handleInput(sess, ac, raw) | |
| @@ -240,7 +240,7 @@ func TestFloatingMouseIgnoresBorderAndOutside(t *testing.T) { | |||
| 240 | 240 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | |
| 241 | 241 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | |
| 242 | 242 | installTestFloating(tb, floating, true) | |
| 243 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 20}, d.currentFloatingConfig()) | |
| 243 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 20}, d.currentFloatingConfig()) | |
| 244 | 244 | ||
| 245 | 245 | border := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Bounds.X+1, geometry.Bounds.Y+2)) | |
| 246 | 246 | outside := []byte("\x1b[<0;1;2M") | |
| @@ -223,7 +223,7 @@ func TestFloatingMouseTranslatesSGRToInnerCoordinates(t *testing.T) { | |||||
| 223 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | 223 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | ||
| 224 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | 224 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | ||
| 225 | installTestFloating(tb, floating, true) | 225 | installTestFloating(tb, floating, true) | ||
| 226 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 20}, d.currentFloatingConfig()) | 226 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 20}, d.currentFloatingConfig()) |
| 227 | raw := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Inner.X+1, geometry.Inner.Y+2)) | 227 | raw := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Inner.X+1, geometry.Inner.Y+2)) | ||
| 228 | 228 | ||||
| 229 | d.handleInput(sess, ac, raw) | 229 | d.handleInput(sess, ac, raw) | ||
| @@ -240,7 +240,7 @@ func TestFloatingMouseIgnoresBorderAndOutside(t *testing.T) { | |||||
| 240 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | 240 | floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14}) | ||
| 241 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | 241 | floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h")) | ||
| 242 | installTestFloating(tb, floating, true) | 242 | installTestFloating(tb, floating, true) | ||
| 243 | - | geometry := calculateFloatingGeometry(domain.Rect{Width: 100, Height: 20}, d.currentFloatingConfig()) | 243 | + | geometry := calculateContentFloatingGeometry(domain.Size{Cols: 100, Rows: 20}, d.currentFloatingConfig()) |
| 244 | 244 | ||||
| 245 | border := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Bounds.X+1, geometry.Bounds.Y+2)) | 245 | border := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Bounds.X+1, geometry.Bounds.Y+2)) | ||
| 246 | outside := []byte("\x1b[<0;1;2M") | 246 | outside := []byte("\x1b[<0;1;2M") | ||
M internal/usecase/daemon/render.go +1 -1
| @@ -397,7 +397,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 397 | 397 | contentArea := domain.Rect{Y: 1, Width: frame.Width, Height: max(0, frame.Height-2)} | |
| 398 | 398 | floatingFrameGeometry := floatingGeometry{} | |
| 399 | 399 | if hasFloating { | |
| 400 | - | desiredFloatingGeometry := calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, floatingCfg) | |
| 400 | + | desiredFloatingGeometry := calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, floatingCfg) | |
| 401 | 401 | frame, damage, floatingFrameGeometry = composeFloatingFrame(frame, damage, floating, tb.floating.generation, contentArea, desiredFloatingGeometry, layoutSnap, bars.theme, &ac.composed, reset || overlayActive) | |
| 402 | 402 | } | |
| 403 | 403 | if overlays.copyActive { | |
| @@ -397,7 +397,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 397 | contentArea := domain.Rect{Y: 1, Width: frame.Width, Height: max(0, frame.Height-2)} | 397 | contentArea := domain.Rect{Y: 1, Width: frame.Width, Height: max(0, frame.Height-2)} | ||
| 398 | floatingFrameGeometry := floatingGeometry{} | 398 | floatingFrameGeometry := floatingGeometry{} | ||
| 399 | if hasFloating { | 399 | if hasFloating { | ||
| 400 | - | desiredFloatingGeometry := calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, floatingCfg) | 400 | + | desiredFloatingGeometry := calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, floatingCfg) |
| 401 | frame, damage, floatingFrameGeometry = composeFloatingFrame(frame, damage, floating, tb.floating.generation, contentArea, desiredFloatingGeometry, layoutSnap, bars.theme, &ac.composed, reset || overlayActive) | 401 | frame, damage, floatingFrameGeometry = composeFloatingFrame(frame, damage, floating, tb.floating.generation, contentArea, desiredFloatingGeometry, layoutSnap, bars.theme, &ac.composed, reset || overlayActive) | ||
| 402 | } | 402 | } | ||
| 403 | if overlays.copyActive { | 403 | if overlays.copyActive { | ||
M internal/usecase/daemon/render_test.go +3 -3
| @@ -1311,7 +1311,7 @@ func TestPaintAlignsFloatingCursorWithCommittedGeometry(t *testing.T) { | |||
| 1311 | 1311 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 1312 | 1312 | contentArea := domain.Rect{Y: 1, Width: 80, Height: 23} | |
| 1313 | 1313 | cfg := d.currentFloatingConfig() | |
| 1314 | - | committed := calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg) | |
| 1314 | + | committed := calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg) | |
| 1315 | 1315 | floating := newPane("floating", nil, rectSize(committed.Inner)) | |
| 1316 | 1316 | floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'}) | |
| 1317 | 1317 | floating.popupGeometry = committed | |
| @@ -1482,7 +1482,7 @@ func TestCopyTargetRectLocked(t *testing.T) { | |||
| 1482 | 1482 | defer tb.mu.Unlock() | |
| 1483 | 1483 | layoutSnap := solveTabLayoutLocked(tb) | |
| 1484 | 1484 | main := tb.focusedPane() | |
| 1485 | - | fp.popupGeometry = calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg) | |
| 1485 | + | fp.popupGeometry = calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg) | |
| 1486 | 1486 | floatingFrameGeometry := fp.popupGeometry.translate(contentArea.X, contentArea.Y) | |
| 1487 | 1487 | ||
| 1488 | 1488 | cases := []struct { | |
| @@ -1492,7 +1492,7 @@ func TestCopyTargetRectLocked(t *testing.T) { | |||
| 1492 | 1492 | hasFloating bool | |
| 1493 | 1493 | want domain.Rect | |
| 1494 | 1494 | }{ | |
| 1495 | - | {name: "floating source targets committed frame-absolute popup inner", pane: fp, floating: fp, hasFloating: true, want: calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg).translate(contentArea.X, contentArea.Y).Inner}, | |
| 1495 | + | {name: "floating source targets committed frame-absolute popup inner", pane: fp, floating: fp, hasFloating: true, want: calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg).translate(contentArea.X, contentArea.Y).Inner}, | |
| 1496 | 1496 | {name: "normal source targets solved placement", pane: main, want: domain.Rect{X: 0, Y: 1, Width: 80, Height: 23}}, | |
| 1497 | 1497 | {name: "unplaced source falls back to pane frame", pane: stray, want: domain.Rect{X: 0, Y: 1, Width: 10, Height: 4}}, | |
| 1498 | 1498 | } | |
| @@ -1311,7 +1311,7 @@ func TestPaintAlignsFloatingCursorWithCommittedGeometry(t *testing.T) { | |||||
| 1311 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | 1311 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | ||
| 1312 | contentArea := domain.Rect{Y: 1, Width: 80, Height: 23} | 1312 | contentArea := domain.Rect{Y: 1, Width: 80, Height: 23} | ||
| 1313 | cfg := d.currentFloatingConfig() | 1313 | cfg := d.currentFloatingConfig() | ||
| 1314 | - | committed := calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg) | 1314 | + | committed := calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg) |
| 1315 | floating := newPane("floating", nil, rectSize(committed.Inner)) | 1315 | floating := newPane("floating", nil, rectSize(committed.Inner)) | ||
| 1316 | floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'}) | 1316 | floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'}) | ||
| 1317 | floating.popupGeometry = committed | 1317 | floating.popupGeometry = committed | ||
| @@ -1482,7 +1482,7 @@ func TestCopyTargetRectLocked(t *testing.T) { | |||||
| 1482 | defer tb.mu.Unlock() | 1482 | defer tb.mu.Unlock() | ||
| 1483 | layoutSnap := solveTabLayoutLocked(tb) | 1483 | layoutSnap := solveTabLayoutLocked(tb) | ||
| 1484 | main := tb.focusedPane() | 1484 | main := tb.focusedPane() | ||
| 1485 | - | fp.popupGeometry = calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg) | 1485 | + | fp.popupGeometry = calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg) |
| 1486 | floatingFrameGeometry := fp.popupGeometry.translate(contentArea.X, contentArea.Y) | 1486 | floatingFrameGeometry := fp.popupGeometry.translate(contentArea.X, contentArea.Y) | ||
| 1487 | 1487 | ||||
| 1488 | cases := []struct { | 1488 | cases := []struct { | ||
| @@ -1492,7 +1492,7 @@ func TestCopyTargetRectLocked(t *testing.T) { | |||||
| 1492 | hasFloating bool | 1492 | hasFloating bool | ||
| 1493 | want domain.Rect | 1493 | want domain.Rect | ||
| 1494 | }{ | 1494 | }{ | ||
| 1495 | - | {name: "floating source targets committed frame-absolute popup inner", pane: fp, floating: fp, hasFloating: true, want: calculateFloatingGeometry(domain.Rect{Width: contentArea.Width, Height: contentArea.Height}, cfg).translate(contentArea.X, contentArea.Y).Inner}, | 1495 | + | {name: "floating source targets committed frame-absolute popup inner", pane: fp, floating: fp, hasFloating: true, want: calculateContentFloatingGeometry(domain.Size{Cols: contentArea.Width, Rows: contentArea.Height}, cfg).translate(contentArea.X, contentArea.Y).Inner}, |
| 1496 | {name: "normal source targets solved placement", pane: main, want: domain.Rect{X: 0, Y: 1, Width: 80, Height: 23}}, | 1496 | {name: "normal source targets solved placement", pane: main, want: domain.Rect{X: 0, Y: 1, Width: 80, Height: 23}}, | ||
| 1497 | {name: "unplaced source falls back to pane frame", pane: stray, want: domain.Rect{X: 0, Y: 1, Width: 10, Height: 4}}, | 1497 | {name: "unplaced source falls back to pane frame", pane: stray, want: domain.Rect{X: 0, Y: 1, Width: 10, Height: 4}}, | ||
| 1498 | } | 1498 | } | ||