detached 9dd5126

refactor(daemon): make floating geometry content-relative

commit
9dd512665ee25c7fe6e9aec313d2df8879cc4285
author
brice · · 2026-07-11 05:42 UTC
parent
d852566
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
750750 d.enterCopyMode(sess, ac)
751751 awaitFrame(t, sends, ports.MsgOutput)
752752 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
754754
755755 // Copy body row k renders at wire row inner.Y+k+2 (top bar + content
756756 // 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.ViewportTop752 viewportTop := ac.overlays.copyMode.ViewportTop
753- inner := calculateFloatingGeometry(domain.Rect{Width: 80, Height: 23}, d.currentFloatingConfig()).Inner753+ 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 + content755 // 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,
133133 return nil, floatingGeometry{}, false
134134 }
135135 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)
137137 p.mu.Lock()
138138 geometry := p.committedFloatingGeometryLocked(desired)
139139 p.mu.Unlock()
@@ -250,7 +250,7 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati
250250 name, cwd, term, sessCtx := sess.name, sess.cwd, sess.terminal, sess.ctx
251251 sess.mu.Unlock()
252252 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)
254254 size := rectSize(geometry.Inner)
255255 if !size.Valid() {
256256 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{}, false133 return nil, floatingGeometry{}, false
134 }134 }
135 p := tb.floating.pane135 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.ctx250 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 (
88 )
99
1010 // 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.
1313 type floatingGeometry struct {
1414 Bounds domain.Rect
1515 Inner domain.Rect
@@ -56,19 +56,19 @@ func calculateFloatingAxisGeometry(available, percent int) floatingAxisGeometry
5656 return axis
5757 }
5858
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() {
6565 return floatingGeometry{}
6666 }
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)
6969 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,
7272 Width: x.BoundsSize,
7373 Height: y.BoundsSize,
7474 }
@@ -211,12 +211,12 @@ func (d *Daemon) resizeInstalledFloating(tb *tab) bool {
211211 }
212212 tb.mu.Lock()
213213 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}
215215 tb.mu.Unlock()
216216 if p == nil {
217217 return false
218218 }
219- return d.resizeFloatingPane(p, calculateFloatingGeometry(content, d.currentFloatingConfig()))
219+ return d.resizeFloatingPane(p, calculateContentFloatingGeometry(content, d.currentFloatingConfig()))
220220 }
221221
222222 // 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 relative11+// 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.Rect14 Bounds domain.Rect
15 Inner domain.Rect15 Inner domain.Rect
@@ -56,19 +56,19 @@ func calculateFloatingAxisGeometry(available, percent int) floatingAxisGeometry
56 return axis56 return axis
57 }57 }
58 58
59-// calculateFloatingGeometry returns coordinates in the supplied rectangle's59+// calculateContentFloatingGeometry returns origin-zero, tab-content-relative
60-// space and centers a percentage-sized popup in content. Launch sizing derives60+// coordinates for a percentage-sized popup. Launch sizing derives from Inner
61-// from Inner too, so rendering and PTY geometry share61+// 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.pane213 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 false217 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) {
3535 const frameX, frameY = 7, 3
3636 tests := []struct {
3737 name string
38- content domain.Rect
38+ content domain.Size
3939 cfg domain.FloatingConfig
4040 tinyWidth, tinyHeight bool
4141 }{
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}},
4646 }
4747 for _, tt := range tests {
4848 t.Run(tt.name, func(t *testing.T) {
49- contentGeometry := calculateFloatingGeometry(tt.content, tt.cfg)
49+ contentGeometry := calculateContentFloatingGeometry(tt.content, tt.cfg)
5050 frameGeometry := contentGeometry.translate(frameX, frameY)
5151
5252 require.Equal(t, contentGeometry.Bounds.X+frameX, frameGeometry.Bounds.X)
@@ -73,19 +73,19 @@ func TestFloatingFrameGeometryPreservesBorderRules(t *testing.T) {
7373 func TestCalculateFloatingGeometry(t *testing.T) {
7474 tests := []struct {
7575 name string
76- content domain.Rect
76+ content domain.Size
7777 cfg domain.FloatingConfig
7878 want floatingGeometry
7979 }{
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}}},
8585 }
8686 for _, tt := range tests {
8787 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))
8989 })
9090 }
9191 }
@@ -117,9 +117,9 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) {
117117 base.Set(2, 2, renderer.Cell{Rune: 'B'})
118118 content := domain.Rect{Y: 1, Width: 40, Height: 10}
119119 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)
121121 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)
123123 require.Equal(t, '┌', frame.At(geometry.Bounds.X, geometry.Bounds.Y).Rune)
124124 require.Equal(t, 'F', frame.At(geometry.Inner.X, geometry.Inner.Y).Rune)
125125 var gotTitle strings.Builder
@@ -131,7 +131,7 @@ func TestComposeFloatingFrameDoesNotMutateSourceAndDamagesTitle(t *testing.T) {
131131 p.mu.Lock()
132132 p.title.generation++
133133 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)
135135 require.Len(t, damage, 1)
136136 require.Equal(t, geometry.Bounds.Y, damage[0].Y)
137137 require.Equal(t, 1, damage[0].Height)
@@ -220,8 +220,8 @@ func TestComposeFloatingFrameRendersPaneOwnedCommandFallback(t *testing.T) {
220220 base := renderer.NewFrame(40, 12)
221221 content := domain.Rect{Y: 1, Width: 40, Height: 10}
222222 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)
225225 var gotTitle strings.Builder
226226 for x := geometry.Bounds.X + 2; x < geometry.Bounds.X+geometry.Bounds.Width-2; x++ {
227227 gotTitle.WriteRune(frame.At(x, geometry.Bounds.Y).Rune)
@@ -253,7 +253,7 @@ func TestComposeFloatingFrameSynchronizesWithPTYReader(t *testing.T) {
253253 <-start
254254 cache := &composedFrameCache{}
255255 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)
257257 }
258258 }()
259259 close(start)
@@ -310,13 +310,13 @@ func BenchmarkComposeFloatingFrameCached(b *testing.B) {
310310 content := domain.Rect{Y: 1, Width: 80, Height: 22}
311311 cfg := domain.FloatingConfig{Width: 80, Height: 80}
312312 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)
314314 p.screen.ClearDamage()
315315
316316 b.ReportAllocs()
317317 b.ResetTimer()
318318 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)
320320 }
321321 }
322322
@@ -327,11 +327,11 @@ func TestComposeFloatingFrameCachedDoesNotAllocate(t *testing.T) {
327327 content := domain.Rect{Y: 1, Width: 80, Height: 22}
328328 cfg := domain.FloatingConfig{Width: 80, Height: 80}
329329 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)
331331 p.screen.ClearDamage()
332332
333333 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)
335335 })
336336 require.Zero(t, allocs, "cached floating composition must not allocate without damage or title changes")
337337 }
@@ -342,8 +342,8 @@ func TestToggleFloatingResizesHiddenPaneOnShowAndRetriesFailure(t *testing.T) {
342342 tb := newTab(nil, domain.Size{Cols: 80, Rows: 24})
343343 sess := &session{tabs: []*tab{tb}}
344344
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())
347347 initial := initialGeometry.Inner
348348 current := currentGeometry.Inner
349349 pty := &resizePTY{errs: []error{errors.New("first show fails"), nil}}
@@ -384,8 +384,8 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T)
384384 cfg := domain.FloatingConfig{Width: 50, Height: 50}
385385 oldContent := domain.Rect{Y: 1, Width: 80, Height: 24}
386386 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)
389389 pty := &resizePTY{err: errors.New("resize failed")}
390390 p := newPane("floating", pty, rectSize(oldGeometry.Inner))
391391 p.rect = oldGeometry.Inner
@@ -405,7 +405,7 @@ func TestFailedFloatingResizeKeepsCommittedRenderAndInputGeometry(t *testing.T)
405405 require.Equal(t, oldGeometry.translate(newContent.X, newContent.Y).Inner, copyRect)
406406
407407 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)
409409 oldFrameGeometry := oldGeometry.translate(newContent.X, newContent.Y)
410410 newFrameGeometry := newGeometry.translate(newContent.X, newContent.Y)
411411 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, 335 const frameX, frameY = 7, 3
36 tests := []struct {36 tests := []struct {
37 name string37 name string
38- content domain.Rect38+ content domain.Size
39 cfg domain.FloatingConfig39 cfg domain.FloatingConfig
40 tinyWidth, tinyHeight bool40 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 string75 name string
76- content domain.Rect76+ content domain.Size
77 cfg domain.FloatingConfig77 cfg domain.FloatingConfig
78 want floatingGeometry78 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.Builder125 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.Builder225 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 <-start253 <-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.Inner347 initial := initialGeometry.Inner
348 current := currentGeometry.Inner348 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.Inner391 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) {
223223 floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14})
224224 floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
225225 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())
227227 raw := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Inner.X+1, geometry.Inner.Y+2))
228228
229229 d.handleInput(sess, ac, raw)
@@ -240,7 +240,7 @@ func TestFloatingMouseIgnoresBorderAndOutside(t *testing.T) {
240240 floating := newPane("floating", floatingPTY, domain.Size{Cols: 78, Rows: 14})
241241 floating.screen.Write([]byte("\x1b[?1000h\x1b[?1006h"))
242242 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())
244244
245245 border := []byte(fmt.Sprintf("\x1b[<0;%d;%dM", geometry.Bounds.X+1, geometry.Bounds.Y+2))
246246 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) {
397397 contentArea := domain.Rect{Y: 1, Width: frame.Width, Height: max(0, frame.Height-2)}
398398 floatingFrameGeometry := floatingGeometry{}
399399 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)
401401 frame, damage, floatingFrameGeometry = composeFloatingFrame(frame, damage, floating, tb.floating.generation, contentArea, desiredFloatingGeometry, layoutSnap, bars.theme, &ac.composed, reset || overlayActive)
402402 }
403403 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) {
13111311 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
13121312 contentArea := domain.Rect{Y: 1, Width: 80, Height: 23}
13131313 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)
13151315 floating := newPane("floating", nil, rectSize(committed.Inner))
13161316 floating.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'F'})
13171317 floating.popupGeometry = committed
@@ -1482,7 +1482,7 @@ func TestCopyTargetRectLocked(t *testing.T) {
14821482 defer tb.mu.Unlock()
14831483 layoutSnap := solveTabLayoutLocked(tb)
14841484 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)
14861486 floatingFrameGeometry := fp.popupGeometry.translate(contentArea.X, contentArea.Y)
14871487
14881488 cases := []struct {
@@ -1492,7 +1492,7 @@ func TestCopyTargetRectLocked(t *testing.T) {
14921492 hasFloating bool
14931493 want domain.Rect
14941494 }{
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},
14961496 {name: "normal source targets solved placement", pane: main, want: domain.Rect{X: 0, Y: 1, Width: 80, Height: 23}},
14971497 {name: "unplaced source falls back to pane frame", pane: stray, want: domain.Rect{X: 0, Y: 1, Width: 10, Height: 4}},
14981498 }
@@ -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 = committed1317 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 bool1492 hasFloating bool
1493 want domain.Rect1493 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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close