Skip to content

Commit 74a02db

Browse files
Draw scrollbar in ScrollablePane.
1 parent a9dc22d commit 74a02db

File tree

1 file changed

+118
-8
lines changed

1 file changed

+118
-8
lines changed

prompt_toolkit/layout/scrollable_pane.py

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from prompt_toolkit.key_binding import KeyBindingsBase
66

77
from .containers import Container, ScrollOffsets
8-
from .dimension import AnyDimension, Dimension, to_dimension
8+
from .dimension import AnyDimension, Dimension, sum_layout_dimensions, to_dimension
99
from .mouse_handlers import MouseHandlers
1010
from .screen import Char, Screen, WritePosition
1111

@@ -45,6 +45,7 @@ class ScrollablePane(Container):
4545
for performance reasons.
4646
:param width: When given, use this width instead of looking at the children.
4747
: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.
4849
"""
4950

5051
def __init__(
@@ -56,6 +57,10 @@ def __init__(
5657
max_available_height: int = MAX_AVAILABLE_HEIGHT,
5758
width: AnyDimension = None,
5859
height: AnyDimension = None,
60+
show_scrollbar: FilterOrBool = True,
61+
display_arrows: FilterOrBool = True,
62+
up_arrow_symbol: str = "^",
63+
down_arrow_symbol: str = "v",
5964
) -> None:
6065
self.content = content
6166
self.scroll_offsets = scroll_offsets or ScrollOffsets(top=1, bottom=1)
@@ -64,6 +69,10 @@ def __init__(
6469
self.max_available_height = max_available_height
6570
self.width = width
6671
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
6776

6877
self.vertical_scroll = 0
6978

@@ -79,14 +88,24 @@ def preferred_width(self, max_available_width: int) -> Dimension:
7988

8089
# We're only scrolling vertical. So the preferred width is equal to
8190
# 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
8398

8499
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
85100
if self.height is not None:
86101
return to_dimension(self.height)
87102

88103
# Prefer a height large enough so that it fits all the content. If not,
89104
# 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+
90109
dimension = self.content.preferred_height(width, self.max_available_height)
91110

92111
# Only take 'preferred' into account. Min/max can be anything.
@@ -101,11 +120,23 @@ def write_to_screen(
101120
erase_bg: bool,
102121
z_index: Optional[int],
103122
) -> 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+
104136
# Compute preferred height again.
105137
virtual_height = self.content.preferred_height(
106-
write_position.width, self.max_available_height
138+
virtual_width, self.max_available_height
107139
).preferred
108-
virtual_width = write_position.width
109140

110141
# Ensure virtual height is at least the available height.
111142
virtual_height = max(virtual_height, write_position.height)
@@ -130,8 +161,6 @@ def write_to_screen(
130161
)
131162
temp_screen.draw_all_floats()
132163

133-
# TODO: draw scrollbar?
134-
135164
# If anything in the virtual screen is focused, move vertical scroll to
136165
from prompt_toolkit.application import get_app
137166

@@ -164,14 +193,14 @@ def write_to_screen(
164193
]
165194
zero_width_escapes = screen.zero_width_escapes[y + ypos]
166195

167-
for x in range(write_position.width):
196+
for x in range(virtual_width):
168197
row[x + xpos] = temp_row[x]
169198

170199
if x in temp_zero_width_escapes:
171200
zero_width_escapes[x + xpos] = temp_zero_width_escapes[x]
172201

173202
# Set screen.width/height.
174-
screen.width = max(screen.width, xpos + write_position.width)
203+
screen.width = max(screen.width, xpos + virtual_width)
175204
screen.height = max(screen.height, ypos + write_position.height)
176205

177206
for win, write_pos in temp_screen.visible_windows_to_write_positions.items():
@@ -199,6 +228,14 @@ def write_to_screen(
199228
x=point.x + xpos, y=point.y + ypos - self.vertical_scroll
200229
)
201230

231+
# Draw scrollbar.
232+
if show_scrollbar:
233+
self._draw_scrollbar(
234+
write_position,
235+
virtual_height,
236+
screen,
237+
)
238+
202239
def is_modal(self) -> bool:
203240
return self.content.is_modal()
204241

@@ -273,3 +310,76 @@ def _make_window_visible(
273310
self.vertical_scroll = max_scroll
274311
if self.vertical_scroll < min_scroll:
275312
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

Comments
 (0)