Skip to content

Commit 9a895fe

Browse files
Added ScrollablePane: a scrollable layout Container.
This allows applications to build a layout, larger than the terminal, with a vertical scroll bar. The vertical scrolling will be done automatically when certain widgets receive the focus.
1 parent 83a1ab9 commit 9a895fe

File tree

10 files changed

+690
-10
lines changed

10 files changed

+690
-10
lines changed

docs/pages/full_screen_apps.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ responsible for generating the actual content.
168168
| | :class:`~prompt_toolkit.layout.VSplit` |
169169
| | :class:`~prompt_toolkit.layout.FloatContainer` |
170170
| | :class:`~prompt_toolkit.layout.Window` |
171+
| | :class:`~prompt_toolkit.layout.ScrollablePane` |
171172
+---------------------------------------------+------------------------------------------------------+
172173
| :class:`~prompt_toolkit.layout.UIControl` | :class:`~prompt_toolkit.layout.BufferControl` |
173174
| | :class:`~prompt_toolkit.layout.FormattedTextControl` |
@@ -229,6 +230,10 @@ If you want to make some part of the layout only visible when a certain
229230
condition is satisfied, use a
230231
:class:`~prompt_toolkit.layout.ConditionalContainer`.
231232

233+
Finally, there is :class:`~prompt_toolkit.layout.ScrollablePane`, a container
234+
class that can be used to create long forms or nested layouts that are
235+
scrollable as a whole.
236+
232237

233238
Focusing windows
234239
^^^^^^^^^^^^^^^^^

docs/pages/reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ Containers
164164

165165
.. autoclass:: prompt_toolkit.layout.DynamicContainer
166166

167+
.. autoclass:: prompt_toolkit.layout.ScrollablePane
168+
167169
.. autoclass:: prompt_toolkit.layout.ScrollOffsets
168170

169171
.. autoclass:: prompt_toolkit.layout.ColorColumn
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python
2+
"""
3+
A simple example of a scrollable pane.
4+
"""
5+
from prompt_toolkit.application import Application
6+
from prompt_toolkit.application.current import get_app
7+
from prompt_toolkit.key_binding import KeyBindings
8+
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
9+
from prompt_toolkit.layout import Dimension, HSplit, Layout, ScrollablePane
10+
from prompt_toolkit.widgets import Frame, Label, TextArea
11+
12+
13+
def main():
14+
# Create a big layout of many text areas, then wrap them in a `ScrollablePane`.
15+
root_container = Frame(
16+
ScrollablePane(
17+
HSplit(
18+
[
19+
Frame(TextArea(text=f"label-{i}"), width=Dimension())
20+
for i in range(20)
21+
]
22+
)
23+
)
24+
# ScrollablePane(HSplit([TextArea(text=f"label-{i}") for i in range(20)]))
25+
)
26+
27+
layout = Layout(container=root_container)
28+
29+
# Key bindings.
30+
kb = KeyBindings()
31+
32+
@kb.add("c-c")
33+
def exit(event) -> None:
34+
get_app().exit()
35+
36+
kb.add("tab")(focus_next)
37+
kb.add("s-tab")(focus_previous)
38+
39+
# Create and run application.
40+
application = Application(layout=layout, key_bindings=kb, full_screen=True)
41+
application.run()
42+
43+
44+
if __name__ == "__main__":
45+
main()
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python
2+
"""
3+
A simple example of a scrollable pane.
4+
"""
5+
from prompt_toolkit.application import Application
6+
from prompt_toolkit.application.current import get_app
7+
from prompt_toolkit.completion import WordCompleter
8+
from prompt_toolkit.key_binding import KeyBindings
9+
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
10+
from prompt_toolkit.layout import (
11+
CompletionsMenu,
12+
Float,
13+
FloatContainer,
14+
HSplit,
15+
Layout,
16+
ScrollablePane,
17+
VSplit,
18+
)
19+
from prompt_toolkit.widgets import Frame, Label, TextArea
20+
21+
22+
def main():
23+
# Create a big layout of many text areas, then wrap them in a `ScrollablePane`.
24+
root_container = VSplit(
25+
[
26+
Label("<left column>"),
27+
HSplit(
28+
[
29+
Label("ScrollContainer Demo"),
30+
Frame(
31+
ScrollablePane(
32+
HSplit(
33+
[
34+
Frame(
35+
TextArea(
36+
text=f"label-{i}",
37+
completer=animal_completer,
38+
)
39+
)
40+
for i in range(20)
41+
]
42+
)
43+
),
44+
),
45+
]
46+
),
47+
]
48+
)
49+
50+
root_container = FloatContainer(
51+
root_container,
52+
floats=[
53+
Float(
54+
xcursor=True,
55+
ycursor=True,
56+
content=CompletionsMenu(max_height=16, scroll_offset=1),
57+
),
58+
],
59+
)
60+
61+
layout = Layout(container=root_container)
62+
63+
# Key bindings.
64+
kb = KeyBindings()
65+
66+
@kb.add("c-c")
67+
def exit(event) -> None:
68+
get_app().exit()
69+
70+
kb.add("tab")(focus_next)
71+
kb.add("s-tab")(focus_previous)
72+
73+
# Create and run application.
74+
application = Application(
75+
layout=layout, key_bindings=kb, full_screen=True, mouse_support=True
76+
)
77+
application.run()
78+
79+
80+
animal_completer = WordCompleter(
81+
[
82+
"alligator",
83+
"ant",
84+
"ape",
85+
"bat",
86+
"bear",
87+
"beaver",
88+
"bee",
89+
"bison",
90+
"butterfly",
91+
"cat",
92+
"chicken",
93+
"crocodile",
94+
"dinosaur",
95+
"dog",
96+
"dolphin",
97+
"dove",
98+
"duck",
99+
"eagle",
100+
"elephant",
101+
"fish",
102+
"goat",
103+
"gorilla",
104+
"kangaroo",
105+
"leopard",
106+
"lion",
107+
"mouse",
108+
"rabbit",
109+
"rat",
110+
"snake",
111+
"spider",
112+
"turkey",
113+
"turtle",
114+
],
115+
ignore_case=True,
116+
)
117+
118+
119+
if __name__ == "__main__":
120+
main()

prompt_toolkit/key_binding/bindings/mouse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def _(event: E) -> None:
9494
return
9595

9696
# Call the mouse handler from the renderer.
97-
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
97+
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
9898
handler(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type))
9999

100100
@key_bindings.add(Keys.ScrollUp)
@@ -141,7 +141,7 @@ def _mouse(event: E) -> None:
141141
y -= rows_above_cursor
142142

143143
# Call the mouse event handler.
144-
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
144+
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
145145
handler(MouseEvent(position=Point(x=x, y=y), event_type=event_type))
146146

147147
return key_bindings

prompt_toolkit/layout/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
ScrollbarMargin,
9191
)
9292
from .menus import CompletionsMenu, MultiColumnCompletionsMenu
93+
from .scrollable_pane import ScrollablePane
9394

9495
__all__ = [
9596
# Layout.
@@ -123,6 +124,7 @@
123124
"to_window",
124125
"is_container",
125126
"DynamicContainer",
127+
"ScrollablePane",
126128
# Controls.
127129
"BufferControl",
128130
"SearchBufferControl",

prompt_toolkit/layout/containers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,8 +1923,9 @@ def render_margin(m: Margin, width: int) -> UIContent:
19231923
# Apply 'self.style'
19241924
self._apply_style(screen, write_position, parent_style)
19251925

1926-
# Tell the screen that this user control has been painted.
1927-
screen.visible_windows.append(self)
1926+
# Tell the screen that this user control has been painted at this
1927+
# position.
1928+
screen.visible_windows_to_write_positions[self] = write_position
19281929

19291930
def _copy_body(
19301931
self,

prompt_toolkit/layout/mouse_handlers.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from prompt_toolkit.mouse_events import MouseEvent
66

77
__all__ = [
8+
"MouseHandler",
89
"MouseHandlers",
910
]
1011

12+
MouseHandler = Callable[[MouseEvent], None]
13+
1114

1215
class MouseHandlers:
1316
"""
@@ -20,10 +23,14 @@ def dummy_callback(mouse_event: MouseEvent) -> None:
2023
:param mouse_event: `MouseEvent` instance.
2124
"""
2225

23-
# Map (x,y) tuples to handlers.
26+
# NOTE: Previously, the data structure was a dictionary mapping (x,y)
27+
# to the handlers. This however would be more inefficient when copying
28+
# over the mouse handlers of the visible region in the scrollable pane.
29+
30+
# Map y (row) to x (column) to handlers.
2431
self.mouse_handlers: DefaultDict[
25-
Tuple[int, int], Callable[[MouseEvent], None]
26-
] = defaultdict(lambda: dummy_callback)
32+
int, DefaultDict[int, MouseHandler]
33+
] = defaultdict(lambda: defaultdict(lambda: dummy_callback))
2734

2835
def set_mouse_handler_for_range(
2936
self,
@@ -36,5 +43,8 @@ def set_mouse_handler_for_range(
3643
"""
3744
Set mouse handler for a region.
3845
"""
39-
for x, y in product(range(x_min, x_max), range(y_min, y_max)):
40-
self.mouse_handlers[x, y] = handler
46+
for y in range(y_min, y_max):
47+
row = self.mouse_handlers[y]
48+
49+
for x in range(x_min, x_max):
50+
row[x] = handler

prompt_toolkit/layout/screen.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,15 @@ def __init__(
186186

187187
# Windows that have been drawn. (Each `Window` class will add itself to
188188
# this list.)
189-
self.visible_windows: List["Window"] = []
189+
self.visible_windows_to_write_positions: Dict["Window", "WritePosition"] = {}
190190

191191
# List of (z_index, draw_func)
192192
self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = []
193193

194+
@property
195+
def visible_windows(self) -> List["Window"]:
196+
return list(self.visible_windows_to_write_positions.keys())
197+
194198
def set_cursor_position(self, window: "Window", position: Point) -> None:
195199
"""
196200
Set the cursor position for a given window.

0 commit comments

Comments
 (0)