fix(overlay): keep the app-set cursor shape on modal carets when the pane cursor is hidden
- commit
- ee31e8beacd50877843178ab650faa945aeb0dd7
- author
- brice · · 2026-10-07 20:35 UTC
- parent
- 28cbe10
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- feat/overlay-input-caret
2 files · +13 -7
M internal/usecase/daemon/overlay_cursor_test.go +10 -4
| @@ -15,11 +15,11 @@ func TestOverlayInputCursor(t *testing.T) { | |||
| 15 | 15 | return capturedModal{active: true, presentation: ui.Presentation{Inner: inner}, caretCol: col, hasCaret: true} | |
| 16 | 16 | } | |
| 17 | 17 | notices := capturedModal{active: true, presentation: ui.Presentation{Inner: inner}} | |
| 18 | - | bar := cursorOut{valid: true, row: 5, col: 5, style: 6, hasStyle: true} | |
| 18 | + | bar := capturedCursorInputs{style: 6, hasStyle: true, visible: true, renderable: true} | |
| 19 | 19 | for _, tt := range []struct { | |
| 20 | 20 | name string | |
| 21 | 21 | overlays capturedOverlayRenderState | |
| 22 | - | pane cursorOut | |
| 22 | + | pane capturedCursorInputs | |
| 23 | 23 | want cursorOut | |
| 24 | 24 | }{ | |
| 25 | 25 | { | |
| @@ -29,9 +29,15 @@ func TestOverlayInputCursor(t *testing.T) { | |||
| 29 | 29 | want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true}, | |
| 30 | 30 | }, | |
| 31 | 31 | { | |
| 32 | - | name: "hidden pane cursor falls back to the default shape", | |
| 32 | + | name: "hidden pane cursor keeps its app-set shape", | |
| 33 | 33 | overlays: capturedOverlayRenderState{palette: withCaret(4)}, | |
| 34 | - | pane: cursorOut{hidden: true}, | |
| 34 | + | pane: capturedCursorInputs{style: 6, hasStyle: true}, | |
| 35 | + | want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true}, | |
| 36 | + | }, | |
| 37 | + | { | |
| 38 | + | name: "unstyled pane cursor falls back to the default shape", | |
| 39 | + | overlays: capturedOverlayRenderState{palette: withCaret(4)}, | |
| 40 | + | pane: capturedCursorInputs{visible: true, renderable: true}, | |
| 35 | 41 | want: cursorOut{valid: true, row: 2, col: 7, style: 1, hasStyle: true}, | |
| 36 | 42 | }, | |
| 37 | 43 | { | |
| @@ -15,11 +15,11 @@ func TestOverlayInputCursor(t *testing.T) { | |||||
| 15 | return capturedModal{active: true, presentation: ui.Presentation{Inner: inner}, caretCol: col, hasCaret: true} | 15 | return capturedModal{active: true, presentation: ui.Presentation{Inner: inner}, caretCol: col, hasCaret: true} | ||
| 16 | } | 16 | } | ||
| 17 | notices := capturedModal{active: true, presentation: ui.Presentation{Inner: inner}} | 17 | notices := capturedModal{active: true, presentation: ui.Presentation{Inner: inner}} | ||
| 18 | - | bar := cursorOut{valid: true, row: 5, col: 5, style: 6, hasStyle: true} | 18 | + | bar := capturedCursorInputs{style: 6, hasStyle: true, visible: true, renderable: true} |
| 19 | for _, tt := range []struct { | 19 | for _, tt := range []struct { | ||
| 20 | name string | 20 | name string | ||
| 21 | overlays capturedOverlayRenderState | 21 | overlays capturedOverlayRenderState | ||
| 22 | - | pane cursorOut | 22 | + | pane capturedCursorInputs |
| 23 | want cursorOut | 23 | want cursorOut | ||
| 24 | }{ | 24 | }{ | ||
| 25 | { | 25 | { | ||
| @@ -29,9 +29,15 @@ func TestOverlayInputCursor(t *testing.T) { | |||||
| 29 | want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true}, | 29 | want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true}, | ||
| 30 | }, | 30 | }, | ||
| 31 | { | 31 | { | ||
| 32 | - | name: "hidden pane cursor falls back to the default shape", | 32 | + | name: "hidden pane cursor keeps its app-set shape", |
| 33 | overlays: capturedOverlayRenderState{palette: withCaret(4)}, | 33 | overlays: capturedOverlayRenderState{palette: withCaret(4)}, | ||
| 34 | - | pane: cursorOut{hidden: true}, | 34 | + | pane: capturedCursorInputs{style: 6, hasStyle: true}, |
| 35 | + | want: cursorOut{valid: true, row: 2, col: 7, style: 6, hasStyle: true}, | |||
| 36 | + | }, | |||
| 37 | + | { | |||
| 38 | + | name: "unstyled pane cursor falls back to the default shape", | |||
| 39 | + | overlays: capturedOverlayRenderState{palette: withCaret(4)}, | |||
| 40 | + | pane: capturedCursorInputs{visible: true, renderable: true}, | |||
| 35 | want: cursorOut{valid: true, row: 2, col: 7, style: 1, hasStyle: true}, | 41 | want: cursorOut{valid: true, row: 2, col: 7, style: 1, hasStyle: true}, | ||
| 36 | }, | 42 | }, | ||
| 37 | { | 43 | { | ||
M internal/usecase/daemon/render_pipeline.go +3 -3
| @@ -246,7 +246,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||
| 246 | 246 | } | |
| 247 | 247 | cursor := desiredCapturedCursor(state.cursor, contentY) | |
| 248 | 248 | if overlaysActive { | |
| 249 | - | cursor = overlayInputCursor(state.overlays, cursor, width, frameHeight) | |
| 249 | + | cursor = overlayInputCursor(state.overlays, state.cursor, width, frameHeight) | |
| 250 | 250 | } | |
| 251 | 251 | // A plain copy viewport is painted on a clone or the spare viewport page, | |
| 252 | 252 | // never into baseFrame. Keep | |
| @@ -296,7 +296,7 @@ func desiredCapturedCursor(c capturedCursorInputs, contentY int) cursorOut { | |||
| 296 | 296 | // A real cursor (rather than a drawn caret) lets client predictive echo type | |
| 297 | 297 | // ahead on the input line. Any other overlay hides the cursor. The caret keeps | |
| 298 | 298 | // the focused pane's cursor shape, so opening a modal never changes it. | |
| 299 | - | func overlayInputCursor(o capturedOverlayRenderState, pane cursorOut, width, height int) cursorOut { | |
| 299 | + | func overlayInputCursor(o capturedOverlayRenderState, pane capturedCursorInputs, width, height int) cursorOut { | |
| 300 | 300 | for _, modal := range []capturedModal{o.prompt, o.palette, o.noticesOverlay, o.copySearch} { | |
| 301 | 301 | if !modal.active { | |
| 302 | 302 | continue | |
| @@ -306,7 +306,7 @@ func overlayInputCursor(o capturedOverlayRenderState, pane cursorOut, width, hei | |||
| 306 | 306 | break | |
| 307 | 307 | } | |
| 308 | 308 | style := 1 | |
| 309 | - | if pane.valid { | |
| 309 | + | if pane.hasStyle { | |
| 310 | 310 | style = pane.style | |
| 311 | 311 | } | |
| 312 | 312 | return cursorOut{valid: true, row: row, col: col, style: style, hasStyle: true} | |
| @@ -246,7 +246,7 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||||
| 246 | } | 246 | } | ||
| 247 | cursor := desiredCapturedCursor(state.cursor, contentY) | 247 | cursor := desiredCapturedCursor(state.cursor, contentY) | ||
| 248 | if overlaysActive { | 248 | if overlaysActive { | ||
| 249 | - | cursor = overlayInputCursor(state.overlays, cursor, width, frameHeight) | 249 | + | cursor = overlayInputCursor(state.overlays, state.cursor, width, frameHeight) |
| 250 | } | 250 | } | ||
| 251 | // A plain copy viewport is painted on a clone or the spare viewport page, | 251 | // A plain copy viewport is painted on a clone or the spare viewport page, | ||
| 252 | // never into baseFrame. Keep | 252 | // never into baseFrame. Keep | ||
| @@ -296,7 +296,7 @@ func desiredCapturedCursor(c capturedCursorInputs, contentY int) cursorOut { | |||||
| 296 | // A real cursor (rather than a drawn caret) lets client predictive echo type | 296 | // A real cursor (rather than a drawn caret) lets client predictive echo type | ||
| 297 | // ahead on the input line. Any other overlay hides the cursor. The caret keeps | 297 | // ahead on the input line. Any other overlay hides the cursor. The caret keeps | ||
| 298 | // the focused pane's cursor shape, so opening a modal never changes it. | 298 | // the focused pane's cursor shape, so opening a modal never changes it. | ||
| 299 | - | func overlayInputCursor(o capturedOverlayRenderState, pane cursorOut, width, height int) cursorOut { | 299 | + | func overlayInputCursor(o capturedOverlayRenderState, pane capturedCursorInputs, width, height int) cursorOut { |
| 300 | for _, modal := range []capturedModal{o.prompt, o.palette, o.noticesOverlay, o.copySearch} { | 300 | for _, modal := range []capturedModal{o.prompt, o.palette, o.noticesOverlay, o.copySearch} { | ||
| 301 | if !modal.active { | 301 | if !modal.active { | ||
| 302 | continue | 302 | continue | ||
| @@ -306,7 +306,7 @@ func overlayInputCursor(o capturedOverlayRenderState, pane cursorOut, width, hei | |||||
| 306 | break | 306 | break | ||
| 307 | } | 307 | } | ||
| 308 | style := 1 | 308 | style := 1 | ||
| 309 | - | if pane.valid { | 309 | + | if pane.hasStyle { |
| 310 | style = pane.style | 310 | style = pane.style | ||
| 311 | } | 311 | } | ||
| 312 | return cursorOut{valid: true, row: row, col: col, style: style, hasStyle: true} | 312 | return cursorOut{valid: true, row: row, col: col, style: style, hasStyle: true} | ||