5
5
from prompt_toolkit .key_binding import KeyBindingsBase
6
6
7
7
from .containers import Container , ScrollOffsets
8
- from .dimension import AnyDimension , Dimension , to_dimension
8
+ from .dimension import AnyDimension , Dimension , sum_layout_dimensions , to_dimension
9
9
from .mouse_handlers import MouseHandlers
10
10
from .screen import Char , Screen , WritePosition
11
11
@@ -45,6 +45,7 @@ class ScrollablePane(Container):
45
45
for performance reasons.
46
46
:param width: When given, use this width instead of looking at the children.
47
47
:param height: When given, use this height instead of looking at the children.
48
+ :param show_scrollbar: When `True` display a scrollbar on the right.
48
49
"""
49
50
50
51
def __init__ (
@@ -56,6 +57,10 @@ def __init__(
56
57
max_available_height : int = MAX_AVAILABLE_HEIGHT ,
57
58
width : AnyDimension = None ,
58
59
height : AnyDimension = None ,
60
+ show_scrollbar : FilterOrBool = True ,
61
+ display_arrows : FilterOrBool = True ,
62
+ up_arrow_symbol : str = "^" ,
63
+ down_arrow_symbol : str = "v" ,
59
64
) -> None :
60
65
self .content = content
61
66
self .scroll_offsets = scroll_offsets or ScrollOffsets (top = 1 , bottom = 1 )
@@ -64,6 +69,10 @@ def __init__(
64
69
self .max_available_height = max_available_height
65
70
self .width = width
66
71
self .height = height
72
+ self .show_scrollbar = to_filter (show_scrollbar )
73
+ self .display_arrows = to_filter (display_arrows )
74
+ self .up_arrow_symbol = up_arrow_symbol
75
+ self .down_arrow_symbol = down_arrow_symbol
67
76
68
77
self .vertical_scroll = 0
69
78
@@ -79,14 +88,24 @@ def preferred_width(self, max_available_width: int) -> Dimension:
79
88
80
89
# We're only scrolling vertical. So the preferred width is equal to
81
90
# that of the content.
82
- return self .content .preferred_width (max_available_width )
91
+ content_width = self .content .preferred_width (max_available_width )
92
+
93
+ # If a scrollbar needs to be displayed, add +1 to the content width.
94
+ if self .show_scrollbar ():
95
+ return sum_layout_dimensions ([Dimension .exact (1 ), content_width ])
96
+
97
+ return content_width
83
98
84
99
def preferred_height (self , width : int , max_available_height : int ) -> Dimension :
85
100
if self .height is not None :
86
101
return to_dimension (self .height )
87
102
88
103
# Prefer a height large enough so that it fits all the content. If not,
89
104
# we'll make the pane scrollable.
105
+ if self .show_scrollbar ():
106
+ # If `show_scrollbar` is set. Always reserve space for the scrollbar.
107
+ width -= 1
108
+
90
109
dimension = self .content .preferred_height (width , self .max_available_height )
91
110
92
111
# Only take 'preferred' into account. Min/max can be anything.
@@ -101,11 +120,23 @@ def write_to_screen(
101
120
erase_bg : bool ,
102
121
z_index : Optional [int ],
103
122
) -> None :
123
+ """
124
+ Render scrollable pane content.
125
+
126
+ This works by rendering on an off-screen canvas, and copying over the
127
+ visible region.
128
+ """
129
+ show_scrollbar = self .show_scrollbar ()
130
+
131
+ if show_scrollbar :
132
+ virtual_width = write_position .width - 1
133
+ else :
134
+ virtual_width = write_position .width
135
+
104
136
# Compute preferred height again.
105
137
virtual_height = self .content .preferred_height (
106
- write_position . width , self .max_available_height
138
+ virtual_width , self .max_available_height
107
139
).preferred
108
- virtual_width = write_position .width
109
140
110
141
# Ensure virtual height is at least the available height.
111
142
virtual_height = max (virtual_height , write_position .height )
@@ -130,8 +161,6 @@ def write_to_screen(
130
161
)
131
162
temp_screen .draw_all_floats ()
132
163
133
- # TODO: draw scrollbar?
134
-
135
164
# If anything in the virtual screen is focused, move vertical scroll to
136
165
from prompt_toolkit .application import get_app
137
166
@@ -164,14 +193,14 @@ def write_to_screen(
164
193
]
165
194
zero_width_escapes = screen .zero_width_escapes [y + ypos ]
166
195
167
- for x in range (write_position . width ):
196
+ for x in range (virtual_width ):
168
197
row [x + xpos ] = temp_row [x ]
169
198
170
199
if x in temp_zero_width_escapes :
171
200
zero_width_escapes [x + xpos ] = temp_zero_width_escapes [x ]
172
201
173
202
# Set screen.width/height.
174
- screen .width = max (screen .width , xpos + write_position . width )
203
+ screen .width = max (screen .width , xpos + virtual_width )
175
204
screen .height = max (screen .height , ypos + write_position .height )
176
205
177
206
for win , write_pos in temp_screen .visible_windows_to_write_positions .items ():
@@ -199,6 +228,14 @@ def write_to_screen(
199
228
x = point .x + xpos , y = point .y + ypos - self .vertical_scroll
200
229
)
201
230
231
+ # Draw scrollbar.
232
+ if show_scrollbar :
233
+ self ._draw_scrollbar (
234
+ write_position ,
235
+ virtual_height ,
236
+ screen ,
237
+ )
238
+
202
239
def is_modal (self ) -> bool :
203
240
return self .content .is_modal ()
204
241
@@ -273,3 +310,76 @@ def _make_window_visible(
273
310
self .vertical_scroll = max_scroll
274
311
if self .vertical_scroll < min_scroll :
275
312
self .vertical_scroll = min_scroll
313
+
314
+ def _draw_scrollbar (
315
+ self , write_position : WritePosition , content_height : int , screen : Screen
316
+ ) -> None :
317
+ """
318
+ Draw the scrollbar on the screen.
319
+
320
+ Note: There is some code duplication with the `ScrollbarMargin`
321
+ implementation.
322
+ """
323
+
324
+ window_height = write_position .height
325
+ display_arrows = self .display_arrows ()
326
+
327
+ if display_arrows :
328
+ window_height -= 2
329
+
330
+ try :
331
+ fraction_visible = write_position .height / float (content_height )
332
+ fraction_above = self .vertical_scroll / float (content_height )
333
+
334
+ scrollbar_height = int (
335
+ min (window_height , max (1 , window_height * fraction_visible ))
336
+ )
337
+ scrollbar_top = int (window_height * fraction_above )
338
+ except ZeroDivisionError :
339
+ return
340
+ else :
341
+
342
+ def is_scroll_button (row : int ) -> bool :
343
+ " True if we should display a button on this row. "
344
+ return scrollbar_top <= row <= scrollbar_top + scrollbar_height
345
+
346
+ xpos = write_position .xpos + write_position .width - 1
347
+ ypos = write_position .ypos
348
+ data_buffer = screen .data_buffer
349
+
350
+ # Up arrow.
351
+ if display_arrows :
352
+ data_buffer [ypos ][xpos ] = Char (
353
+ self .up_arrow_symbol , "class:scrollbar.arrow"
354
+ )
355
+ ypos += 1
356
+
357
+ # Scrollbar body.
358
+ scrollbar_background = "class:scrollbar.background"
359
+ scrollbar_background_start = "class:scrollbar.background,scrollbar.start"
360
+ scrollbar_button = "class:scrollbar.button"
361
+ scrollbar_button_end = "class:scrollbar.button,scrollbar.end"
362
+
363
+ for i in range (window_height ):
364
+ style = ""
365
+ if is_scroll_button (i ):
366
+ if not is_scroll_button (i + 1 ):
367
+ # Give the last cell a different style, because we want
368
+ # to underline this.
369
+ style = scrollbar_button_end
370
+ else :
371
+ style = scrollbar_button
372
+ else :
373
+ if is_scroll_button (i + 1 ):
374
+ style = scrollbar_background_start
375
+ else :
376
+ style = scrollbar_background
377
+
378
+ data_buffer [ypos ][xpos ] = Char (" " , style )
379
+ ypos += 1
380
+
381
+ # Down arrow
382
+ if display_arrows :
383
+ data_buffer [ypos ][xpos ] = Char (
384
+ self .down_arrow_symbol , "class:scrollbar.arrow"
385
+ )
0 commit comments