Skip to content

Commit bed8b62

Browse files
Don't repaint if mouse handler returns NotImplemented.
1 parent 5daaafb commit bed8b62

File tree

8 files changed

+98
-55
lines changed

8 files changed

+98
-55
lines changed

Diff for: prompt_toolkit/key_binding/bindings/mouse.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import FrozenSet
1+
from typing import TYPE_CHECKING, FrozenSet
22

33
from prompt_toolkit.data_structures import Point
44
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
@@ -13,6 +13,9 @@
1313

1414
from ..key_bindings import KeyBindings
1515

16+
if TYPE_CHECKING:
17+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
18+
1619
__all__ = [
1720
"load_mouse_bindings",
1821
]
@@ -185,7 +188,7 @@ def load_mouse_bindings() -> KeyBindings:
185188
key_bindings = KeyBindings()
186189

187190
@key_bindings.add(Keys.Vt100MouseEvent)
188-
def _(event: E) -> None:
191+
def _(event: E) -> "NotImplementedOrNone":
189192
"""
190193
Handling of incoming mouse event.
191194
"""
@@ -235,7 +238,7 @@ def _(event: E) -> None:
235238
mouse_modifiers,
236239
) = xterm_sgr_mouse_events[mouse_event, m]
237240
except KeyError:
238-
return
241+
return NotImplemented
239242

240243
else:
241244
# Some other terminals, like urxvt, Hyper terminal, ...
@@ -259,11 +262,16 @@ def _(event: E) -> None:
259262
try:
260263
y -= event.app.renderer.rows_above_layout
261264
except HeightIsUnknownError:
262-
return
265+
return NotImplemented
263266

264267
# Call the mouse handler from the renderer.
268+
269+
# Note: This can return `NotImplemented` if no mouse handler was
270+
# found for this position, or if no repainting needs to
271+
# happen. this way, we avoid excessive repaints during mouse
272+
# movements.
265273
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
266-
handler(
274+
return handler(
267275
MouseEvent(
268276
position=Point(x=x, y=y),
269277
event_type=mouse_event_type,
@@ -272,6 +280,8 @@ def _(event: E) -> None:
272280
)
273281
)
274282

283+
return NotImplemented
284+
275285
@key_bindings.add(Keys.ScrollUp)
276286
def _scroll_up(event: E) -> None:
277287
"""
@@ -289,7 +299,7 @@ def _scroll_down(event: E) -> None:
289299
event.key_processor.feed(KeyPress(Keys.Down), first=True)
290300

291301
@key_bindings.add(Keys.WindowsMouseEvent)
292-
def _mouse(event: E) -> None:
302+
def _mouse(event: E) -> "NotImplementedOrNone":
293303
"""
294304
Handling of mouse events for Windows.
295305
"""
@@ -317,8 +327,10 @@ def _mouse(event: E) -> None:
317327
y -= rows_above_cursor
318328

319329
# Call the mouse event handler.
330+
# (Can return `NotImplemented`.)
320331
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
321-
handler(
332+
333+
return handler(
322334
MouseEvent(
323335
position=Point(x=x, y=y),
324336
event_type=event_type,
@@ -327,4 +339,8 @@ def _mouse(event: E) -> None:
327339
)
328340
)
329341

342+
# No mouse handler found. Return `NotImplemented` so that we don't
343+
# invalidate the UI.
344+
return NotImplemented
345+
330346
return key_bindings

Diff for: prompt_toolkit/key_binding/key_bindings.py

+36-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def my_key_binding(event):
3535
kb.add(Keys.A, my_key_binding)
3636
"""
3737
from abc import ABCMeta, abstractmethod, abstractproperty
38+
from inspect import isawaitable
3839
from typing import (
3940
TYPE_CHECKING,
4041
Awaitable,
@@ -53,12 +54,27 @@ def my_key_binding(event):
5354
from prompt_toolkit.filters import FilterOrBool, Never, to_filter
5455
from prompt_toolkit.keys import KEY_ALIASES, Keys
5556

56-
# Avoid circular imports.
5757
if TYPE_CHECKING:
58+
# Avoid circular imports.
5859
from .key_processor import KeyPressEvent
5960

61+
# The only two return values for a mouse hander (and key bindings) are
62+
# `None` and `NotImplemented`. For the type checker it's best to annotate
63+
# this as `object`. (The consumer never expects a more specific instance:
64+
# checking for NotImplemented can be done using `is NotImplemented`.)
65+
NotImplementedOrNone = object
66+
# Other non-working options are:
67+
# * Optional[Literal[NotImplemented]]
68+
# --> Doesn't work, Literal can't take an Any.
69+
# * None
70+
# --> Doesn't work. We can't assign the result of a function that
71+
# returns `None` to a variable.
72+
# * Any
73+
# --> Works, but too broad.
74+
6075

6176
__all__ = [
77+
"NotImplementedOrNone",
6278
"Binding",
6379
"KeyBindingsBase",
6480
"KeyBindings",
@@ -68,7 +84,13 @@ def my_key_binding(event):
6884
"GlobalOnlyKeyBindings",
6985
]
7086

71-
KeyHandlerCallable = Callable[["KeyPressEvent"], Union[None, Awaitable[None]]]
87+
# Key bindings can be regular functions or coroutines.
88+
# In both cases, if they return `NotImplemented`, the UI won't be invalidated.
89+
# This is mainly used in case of mouse move events, to prevent excessive
90+
# repainting during mouse move events.
91+
KeyHandlerCallable = Callable[
92+
["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]]
93+
]
7294

7395

7496
class Binding:
@@ -102,8 +124,18 @@ def call(self, event: "KeyPressEvent") -> None:
102124
result = self.handler(event)
103125

104126
# If the handler is a coroutine, create an asyncio task.
105-
if result is not None:
106-
event.app.create_background_task(result)
127+
if isawaitable(result):
128+
awaitable = cast(Awaitable["NotImplementedOrNone"], result)
129+
130+
async def bg_task() -> None:
131+
result = await awaitable
132+
if result != NotImplemented:
133+
event.app.invalidate()
134+
135+
event.app.create_background_task(bg_task())
136+
137+
elif result != NotImplemented:
138+
event.app.invalidate()
107139

108140
def __repr__(self) -> str:
109141
return "%s(keys=%r, handler=%r)" % (

Diff for: prompt_toolkit/key_binding/key_processor.py

-8
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,9 @@ def get_next() -> KeyPress:
255255
else:
256256
return self.input_queue.popleft()
257257

258-
keys_processed = False
259258
is_flush = False
260259

261260
while not_empty():
262-
keys_processed = True
263-
264261
# Process next key.
265262
key_press = get_next()
266263

@@ -277,16 +274,11 @@ def get_next() -> KeyPress:
277274
# an exception was raised) restart the processor for next time.
278275
self.reset()
279276
self.empty_queue()
280-
app.invalidate()
281277
raise
282278

283279
if not is_flush and not is_cpr:
284280
self.after_key_press.fire()
285281

286-
if keys_processed:
287-
# Invalidate user interface.
288-
app.invalidate()
289-
290282
# Skip timeout if the last key was flush.
291283
if not is_flush:
292284
self._start_timeout()

Diff for: prompt_toolkit/layout/containers.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
if TYPE_CHECKING:
6262
from typing_extensions import Protocol, TypeGuard
6363

64-
NotImplementedOrNone = object
64+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
65+
6566

6667
__all__ = [
6768
"AnyContainer",
@@ -1829,13 +1830,16 @@ def _write_to_screen_at_index(
18291830
self.render_info = render_info
18301831

18311832
# Set mouse handlers.
1832-
def mouse_handler(mouse_event: MouseEvent) -> None:
1833-
"""Wrapper around the mouse_handler of the `UIControl` that turns
1834-
screen coordinates into line coordinates."""
1833+
def mouse_handler(mouse_event: MouseEvent) -> "NotImplementedOrNone":
1834+
"""
1835+
Wrapper around the mouse_handler of the `UIControl` that turns
1836+
screen coordinates into line coordinates.
1837+
Returns `NotImplemented` if no UI invalidation should be done.
1838+
"""
18351839
# Don't handle mouse events outside of the current modal part of
18361840
# the UI.
18371841
if self not in get_app().layout.walk_through_modal_area():
1838-
return
1842+
return NotImplemented
18391843

18401844
# Find row/col position first.
18411845
yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
@@ -1882,7 +1886,9 @@ def mouse_handler(mouse_event: MouseEvent) -> None:
18821886

18831887
# If it returns NotImplemented, handle it here.
18841888
if result == NotImplemented:
1885-
self._mouse_handler(mouse_event)
1889+
result = self._mouse_handler(mouse_event)
1890+
1891+
return result
18861892

18871893
mouse_handlers.set_mouse_handler_for_range(
18881894
x_min=write_position.xpos + sum(left_margin_widths),
@@ -2552,15 +2558,22 @@ def do_scroll(
25522558
),
25532559
)
25542560

2555-
def _mouse_handler(self, mouse_event: MouseEvent) -> None:
2561+
def _mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
25562562
"""
25572563
Mouse handler. Called when the UI control doesn't handle this
25582564
particular event.
2565+
2566+
Return `NotImplemented` if nothing was done as a consequence of this
2567+
key binding (no UI invalidate required in that case).
25592568
"""
25602569
if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
25612570
self._scroll_down()
2571+
return None
25622572
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
25632573
self._scroll_up()
2574+
return None
2575+
2576+
return NotImplemented
25642577

25652578
def _scroll_down(self) -> None:
25662579
"Scroll window down."

Diff for: prompt_toolkit/layout/controls.py

+5-21
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@
3232
split_lines,
3333
)
3434
from prompt_toolkit.lexers import Lexer, SimpleLexer
35-
from prompt_toolkit.mouse_events import (
36-
MouseButton,
37-
MouseEvent,
38-
MouseEventType,
39-
MouseModifier,
40-
)
35+
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
4136
from prompt_toolkit.search import SearchState
4237
from prompt_toolkit.selection import SelectionType
4338
from prompt_toolkit.utils import get_cwidth
@@ -53,23 +48,12 @@
5348
)
5449

5550
if TYPE_CHECKING:
56-
from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
51+
from prompt_toolkit.key_binding.key_bindings import (
52+
KeyBindingsBase,
53+
NotImplementedOrNone,
54+
)
5755
from prompt_toolkit.utils import Event
5856

59-
# The only two return values for a mouse hander are `None` and
60-
# `NotImplemented`. For the type checker it's best to annotate this as
61-
# `object`. (The consumer never expects a more specific instance: checking
62-
# for NotImplemented can be done using `is NotImplemented`.)
63-
NotImplementedOrNone = object
64-
# Other non-working options are:
65-
# * Optional[Literal[NotImplemented]]
66-
# --> Doesn't work, Literal can't take an Any.
67-
# * None
68-
# --> Doesn't work. We can't assign the result of a function that
69-
# returns `None` to a variable.
70-
# * Any
71-
# --> Works, but too broad.
72-
7357

7458
__all__ = [
7559
"BufferControl",

Diff for: prompt_toolkit/layout/menus.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040
from .margins import ScrollbarMargin
4141

4242
if TYPE_CHECKING:
43-
from prompt_toolkit.key_binding.key_bindings import KeyBindings
43+
from prompt_toolkit.key_binding.key_bindings import (
44+
KeyBindings,
45+
NotImplementedOrNone,
46+
)
4447

45-
NotImplementedOrNone = object
4648

4749
__all__ = [
4850
"CompletionsMenu",

Diff for: prompt_toolkit/layout/mouse_handlers.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
from collections import defaultdict
2-
from itertools import product
3-
from typing import Callable, DefaultDict, Tuple
2+
from typing import TYPE_CHECKING, Callable, DefaultDict
43

54
from prompt_toolkit.mouse_events import MouseEvent
65

6+
if TYPE_CHECKING:
7+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
8+
79
__all__ = [
810
"MouseHandler",
911
"MouseHandlers",
1012
]
1113

12-
MouseHandler = Callable[[MouseEvent], None]
14+
15+
MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"]
1316

1417

1518
class MouseHandlers:
@@ -18,10 +21,11 @@ class MouseHandlers:
1821
"""
1922

2023
def __init__(self) -> None:
21-
def dummy_callback(mouse_event: MouseEvent) -> None:
24+
def dummy_callback(mouse_event: MouseEvent) -> "NotImplementedOrNone":
2225
"""
2326
:param mouse_event: `MouseEvent` instance.
2427
"""
28+
return NotImplemented
2529

2630
# NOTE: Previously, the data structure was a dictionary mapping (x,y)
2731
# to the handlers. This however would be more inefficient when copying
@@ -38,7 +42,7 @@ def set_mouse_handler_for_range(
3842
x_max: int,
3943
y_min: int,
4044
y_max: int,
41-
handler: Callable[[MouseEvent], None],
45+
handler: Callable[[MouseEvent], "NotImplementedOrNone"],
4246
) -> None:
4347
"""
4448
Set mouse handler for a region.

Diff for: prompt_toolkit/widgets/menus.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
Window,
1717
)
1818
from prompt_toolkit.layout.controls import FormattedTextControl
19-
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
19+
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
2020
from prompt_toolkit.utils import get_cwidth
2121
from prompt_toolkit.widgets import Shadow
2222

0 commit comments

Comments
 (0)