1212from prompt_toolkit .formatted_text .base import StyleAndTextTuples
1313from prompt_toolkit .key_binding import KeyBindings
1414from prompt_toolkit .layout import HSplit , Layout , Window
15- from prompt_toolkit .layout .containers import VerticalAlign
15+ from prompt_toolkit .layout .containers import Container , VerticalAlign
1616from prompt_toolkit .layout .controls import BufferControl
1717from prompt_toolkit .layout .dimension import Dimension
18+ from prompt_toolkit .layout .mouse_handlers import MouseHandlers
1819from prompt_toolkit .layout .processors import Processor , Transformation , TransformationInput
20+ from prompt_toolkit .layout .screen import Screen , WritePosition
21+ from prompt_toolkit .layout .scrollable_pane import ScrollablePane , ScrollOffsets
1922from prompt_toolkit .search import SearchState
2023from prompt_toolkit .widgets import SearchToolbar
2124
@@ -114,6 +117,33 @@ def apply_search(
114117 previous_line_index = current_line_index
115118
116119
120+ class _HeightTrackingScrollablePane (ScrollablePane ):
121+ """
122+ A copy of ``ScrollablePane`` that remembers the latest rendering height.
123+ """
124+
125+ def __init__ (self , content : Container , ** kwargs ):
126+ super ().__init__ (content = content , ** kwargs )
127+ self .current_height = None
128+
129+ def write_to_screen (
130+ self ,
131+ screen : Screen ,
132+ mouse_handlers : MouseHandlers ,
133+ write_position : WritePosition ,
134+ parent_style : str ,
135+ erase_bg : bool ,
136+ z_index : Optional [int ],
137+ ) -> None :
138+ self .current_height = write_position .height
139+ super ().write_to_screen (screen = screen ,
140+ mouse_handlers = mouse_handlers ,
141+ write_position = write_position ,
142+ parent_style = parent_style ,
143+ erase_bg = erase_bg ,
144+ z_index = z_index )
145+
146+
117147class _MultiSelectPrompt :
118148 """
119149 An interactive multi-select using the terminal, based on Prompt Toolkit.
@@ -153,14 +183,15 @@ def __init__(self,
153183 self ._header_lines : [_MultiSelectPrompt .HeaderLine ] = []
154184 self ._footer_lines : [_MultiSelectPrompt .PlainLine ] = []
155185
156- self ._item_selection_window : Optional [Window ] = None
186+ self ._item_selection_pane : Optional [_HeightTrackingScrollablePane ] = None
157187 self ._buffer : Optional [Buffer ] = None
158188 self ._document : Optional [Document ] = None
159189 self ._accepted_selection : List [Any ] = None
160190
161191 def _move_cursor_one_page_vertically (self , upwards : bool ):
162- render_cursor_line = self ._item_selection_window .render_info .cursor_position .y
163- page_height_in_lines = self ._item_selection_window .render_info .window_height
192+ render_cursor_line = (self ._item_selection_pane .content .render_info .cursor_position .y
193+ - self ._item_selection_pane .vertical_scroll )
194+ page_height_in_lines = self ._item_selection_pane .current_height
164195
165196 if upwards and render_cursor_line > 0 :
166197 new_line_index = self .get_cursor_line () - render_cursor_line
@@ -279,7 +310,7 @@ def _create_text_display_window_for(self, lines: List[_LineBase]) -> Window:
279310 wrap_lines = True ,
280311 height = Dimension (min = len (lines ), max = len (lines )))
281312
282- def _create_layout (self ) -> Tuple [Layout , Window ]:
313+ def _create_layout (self ) -> Tuple [Layout , _HeightTrackingScrollablePane ]:
283314 header = self ._create_text_display_window_for (self ._header_lines )
284315 footer = self ._create_text_display_window_for (self ._footer_lines )
285316
@@ -288,26 +319,31 @@ def _create_layout(self) -> Tuple[Layout, Window]:
288319 input_processors = [_ItemRenderProcessor (prompt = self )],
289320 preview_search = True ,
290321 search_buffer_control = search .control )
291- window_height = Dimension (min = 1 ,
292- max = self ._document .line_count ,
293- preferred = self ._document .line_count )
294- item_selection_window = Window (buffer_control ,
295- always_hide_cursor = True ,
296- wrap_lines = True ,
297- height = window_height )
298- item_selection_window_plus_search = HSplit ([item_selection_window , search ])
299- hsplit = HSplit ([header , item_selection_window_plus_search , footer ],
322+ item_selection_window = Window (buffer_control , always_hide_cursor = True , wrap_lines = True )
323+
324+ pane_scroll_offsets = ScrollOffsets (top = 0 , bottom = 0 )
325+ pane_height = Dimension (min = 1 ,
326+ max = self ._document .line_count ,
327+ preferred = self ._document .line_count )
328+ item_selection_pane = _HeightTrackingScrollablePane (item_selection_window ,
329+ height = pane_height ,
330+ scroll_offsets = pane_scroll_offsets )
331+ item_selection_pane .show_scrollbar = lambda : (item_selection_pane .current_height or 0
332+ ) < self ._document .line_count
333+
334+ item_selection_pane_plus_search = HSplit ([item_selection_pane , search ])
335+ hsplit = HSplit ([header , item_selection_pane_plus_search , footer ],
300336 padding = 1 ,
301337 align = VerticalAlign .TOP )
302- return Layout (hsplit , focused_element = item_selection_window ), item_selection_window
338+ return Layout (hsplit , focused_element = item_selection_pane ), item_selection_pane
303339
304340 def _collect_selected_values (self ):
305341 return [item .value for item in self .item_lines if item .selected ]
306342
307343 def get_selected_values (self ) -> List [Any ]:
308344 self ._document = Document (text = '\n ' .join (line .text for line in self .item_lines ))
309345 self ._buffer = _LineJumpingBuffer (read_only = True , document = self ._document )
310- layout , self ._item_selection_window = self ._create_layout ()
346+ layout , self ._item_selection_pane = self ._create_layout ()
311347 app = Application (key_bindings = self ._create_key_bindings (), layout = layout )
312348
313349 self ._move_cursor_to_line (self ._initial_cursor_item_index )
0 commit comments