@@ -200,7 +200,7 @@ func (s *Select[T]) selectOption() {
200200 break
201201 }
202202 }
203- s .viewport . SetYOffset ( s . selected )
203+ s .ensureCursorVisible ( )
204204}
205205
206206// OptionsFunc sets the options func of the select field.
@@ -426,9 +426,8 @@ func (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
426426 if s .selected < 0 {
427427 s .selected = len (s .filteredOptions ) - 1
428428 s .viewport .GotoBottom ()
429- }
430- if s .selected < s .viewport .YOffset () {
431- s .viewport .SetYOffset (s .selected )
429+ } else {
430+ s .ensureCursorVisible ()
432431 }
433432 s .updateValue ()
434433 case key .Matches (msg , s .keymap .GotoTop ):
@@ -446,11 +445,11 @@ func (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
446445 s .viewport .GotoBottom ()
447446 case key .Matches (msg , s .keymap .HalfPageUp ):
448447 s .selected = max (s .selected - s .viewport .Height ()/ 2 , 0 )
449- s .viewport . HalfPageUp ()
448+ s .ensureCursorVisible ()
450449 s .updateValue ()
451450 case key .Matches (msg , s .keymap .HalfPageDown ):
452451 s .selected = min (s .selected + s .viewport .Height ()/ 2 , len (s .filteredOptions )- 1 )
453- s .viewport . HalfPageDown ()
452+ s .ensureCursorVisible ()
454453 s .updateValue ()
455454 case key .Matches (msg , s .keymap .Down , s .keymap .Right ):
456455 // When filtering we should ignore j/k keybindings
@@ -463,9 +462,8 @@ func (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
463462 if s .selected > len (s .filteredOptions )- 1 {
464463 s .selected = 0
465464 s .viewport .GotoTop ()
466- }
467- if s .selected >= s .viewport .YOffset ()+ s .viewport .Height () {
468- s .viewport .ScrollDown (1 )
465+ } else {
466+ s .ensureCursorVisible ()
469467 }
470468 s .updateValue ()
471469 case key .Matches (msg , s .keymap .Prev ):
@@ -508,10 +506,7 @@ func (s *Select[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
508506 }
509507 }
510508
511- _ , offset , height := s .optionsView ()
512- if offset > - 1 && height > 0 && (offset < s .viewport .YOffset () || height + offset >= s .viewport .YOffset ()+ s .viewport .Height ()) {
513- s .viewport .SetYOffset (offset )
514- }
509+ s .ensureCursorVisible ()
515510 }
516511
517512 return s , cmd
@@ -534,8 +529,8 @@ func (s *Select[T]) updateViewportSize() {
534529 if ss := s .descriptionView (); ss != "" {
535530 yoffset += lipgloss .Height (ss )
536531 }
537- s .viewport .SetYOffset (s .selected )
538532 s .viewport .SetHeight (max (minHeight , s .height - yoffset ))
533+ s .ensureCursorVisible ()
539534 } else {
540535 // If no height is set size the viewport to the number of options.
541536 v , _ , _ := s .optionsView ()
@@ -640,6 +635,43 @@ func (s *Select[T]) optionsView() (string, int, int) {
640635 return sb .String (), cursorOffset , cursorHeight
641636}
642637
638+ // cursorLineOffset computes the line offset and height (in lines) for the
639+ // currently selected option without rendering the full options string.
640+ func (s * Select [T ]) cursorLineOffset () (offset int , height int ) {
641+ for i , option := range s .filteredOptions {
642+ line := s .renderOption (option , s .selected == i )
643+ h := lipgloss .Height (line )
644+ if i < s .selected {
645+ offset += h
646+ }
647+ if i == s .selected {
648+ height = h
649+ return offset , height
650+ }
651+ }
652+ return offset , height
653+ }
654+
655+ // ensureVisible scrolls a viewport the minimum amount so that the region
656+ // [offset, offset+height) is within the visible area.
657+ func ensureVisible (vp * viewport.Model , offset , height int ) {
658+ if height <= 0 {
659+ return
660+ }
661+ yOff := vp .YOffset ()
662+ vHeight := vp .Height ()
663+ if offset < yOff {
664+ vp .ScrollUp (yOff - offset )
665+ } else if offset + height > yOff + vHeight {
666+ vp .ScrollDown (offset + height - yOff - vHeight )
667+ }
668+ }
669+
670+ func (s * Select [T ]) ensureCursorVisible () {
671+ offset , height := s .cursorLineOffset ()
672+ ensureVisible (& s .viewport , offset , height )
673+ }
674+
643675func (s * Select [T ]) renderOption (option Option [T ], selected bool ) string {
644676 var (
645677 styles = s .activeStyles ()
0 commit comments