mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
scrolling
This commit is contained in:
@@ -191,7 +191,6 @@ class LinuxDriver(Driver):
|
||||
selector_events = selector.select(0.1)
|
||||
for _selector_key, mask in selector_events:
|
||||
unicode_data = decode(read(fileno, 1024))
|
||||
log.debug(repr(unicode_data))
|
||||
for event in parser.feed(unicode_data):
|
||||
send_event(event)
|
||||
|
||||
|
||||
@@ -97,11 +97,12 @@ class Parser(Generic[T]):
|
||||
pos = 0
|
||||
tokens = self._tokens
|
||||
popleft = tokens.popleft
|
||||
data_size = len(data)
|
||||
|
||||
while tokens:
|
||||
yield popleft()
|
||||
|
||||
while pos < len(data) or isinstance(self._awaiting, PeekBuffer):
|
||||
while pos < data_size or isinstance(self._awaiting, PeekBuffer):
|
||||
|
||||
_awaiting = self._awaiting
|
||||
if isinstance(_awaiting, _Read1):
|
||||
|
||||
@@ -35,9 +35,9 @@ class XTermParser(Parser[events.Event]):
|
||||
|
||||
event_class: Type[events._MouseBase]
|
||||
if buttons & 32:
|
||||
event_class = events.Move
|
||||
event_class = events.MouseMove
|
||||
else:
|
||||
event_class = events.Press if state == "M" else events.Release
|
||||
event_class = events.MouseDown if state == "M" else events.MouseUp
|
||||
button = (4 if (buttons & 64) else 1) + (buttons & 3)
|
||||
event = event_class(
|
||||
sender,
|
||||
@@ -60,7 +60,7 @@ class XTermParser(Parser[events.Event]):
|
||||
|
||||
while not self.is_eof:
|
||||
character = yield read1()
|
||||
if character == ESC and ((yield self.peek_buffer())):
|
||||
if character == ESC and ((yield self.peek_buffer()) or more_data()):
|
||||
sequence: str = character
|
||||
while True:
|
||||
sequence += yield read1()
|
||||
|
||||
@@ -126,6 +126,8 @@ class App(MessagePump):
|
||||
if key_action is not None:
|
||||
log.debug("action %r", key_action)
|
||||
await self.action(key_action)
|
||||
else:
|
||||
await self.view.post_message(event)
|
||||
|
||||
# if event.key == "q":
|
||||
# await self.close_messages()
|
||||
@@ -137,10 +139,13 @@ class App(MessagePump):
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def on_move(self, event: events.Move) -> None:
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def on_click(self, event: events.Click) -> None:
|
||||
async def on_mouse_down(self, event: events.MouseDown) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
await self.view.post_message(event)
|
||||
|
||||
async def action_quit(self, tokens: list[str]) -> None:
|
||||
@@ -165,7 +170,7 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
with open("richreadme.md", "rt") as fh:
|
||||
readme = Markdown(fh.read(), hyperlinks=True)
|
||||
readme = Markdown(fh.read(), hyperlinks=True, code_theme="fruity")
|
||||
|
||||
from rich import print
|
||||
|
||||
@@ -177,6 +182,5 @@ if __name__ == "__main__":
|
||||
await self.view.mount_all(
|
||||
header=Header(self.title), left=Placeholder(), body=Window(readme)
|
||||
)
|
||||
# self.set_timer(3.0, callback=self.close_messages)
|
||||
|
||||
MyApp.run()
|
||||
|
||||
@@ -38,163 +38,155 @@ class Driver(ABC):
|
||||
...
|
||||
|
||||
|
||||
class LinuxDriver(Driver):
|
||||
def start_application_mode(self):
|
||||
pass
|
||||
# class CursesDriver(Driver):
|
||||
|
||||
def stop_application_mode(self):
|
||||
pass
|
||||
# _MOUSE_PRESSED = [
|
||||
# curses.BUTTON1_PRESSED,
|
||||
# curses.BUTTON2_PRESSED,
|
||||
# curses.BUTTON3_PRESSED,
|
||||
# curses.BUTTON4_PRESSED,
|
||||
# ]
|
||||
|
||||
# _MOUSE_RELEASED = [
|
||||
# curses.BUTTON1_RELEASED,
|
||||
# curses.BUTTON2_RELEASED,
|
||||
# curses.BUTTON3_RELEASED,
|
||||
# curses.BUTTON4_RELEASED,
|
||||
# ]
|
||||
|
||||
class CursesDriver(Driver):
|
||||
# _MOUSE_CLICKED = [
|
||||
# curses.BUTTON1_CLICKED,
|
||||
# curses.BUTTON2_CLICKED,
|
||||
# curses.BUTTON3_CLICKED,
|
||||
# curses.BUTTON4_CLICKED,
|
||||
# ]
|
||||
|
||||
_MOUSE_PRESSED = [
|
||||
curses.BUTTON1_PRESSED,
|
||||
curses.BUTTON2_PRESSED,
|
||||
curses.BUTTON3_PRESSED,
|
||||
curses.BUTTON4_PRESSED,
|
||||
]
|
||||
# _MOUSE_DOUBLE_CLICKED = [
|
||||
# curses.BUTTON1_DOUBLE_CLICKED,
|
||||
# curses.BUTTON2_DOUBLE_CLICKED,
|
||||
# curses.BUTTON3_DOUBLE_CLICKED,
|
||||
# curses.BUTTON4_DOUBLE_CLICKED,
|
||||
# ]
|
||||
|
||||
_MOUSE_RELEASED = [
|
||||
curses.BUTTON1_RELEASED,
|
||||
curses.BUTTON2_RELEASED,
|
||||
curses.BUTTON3_RELEASED,
|
||||
curses.BUTTON4_RELEASED,
|
||||
]
|
||||
# _MOUSE = [
|
||||
# (events.MouseDown, _MOUSE_PRESSED),
|
||||
# (events.MouseUp, _MOUSE_RELEASED),
|
||||
# (events.Click, _MOUSE_CLICKED),
|
||||
# (events.DoubleClick, _MOUSE_DOUBLE_CLICKED),
|
||||
# ]
|
||||
|
||||
_MOUSE_CLICKED = [
|
||||
curses.BUTTON1_CLICKED,
|
||||
curses.BUTTON2_CLICKED,
|
||||
curses.BUTTON3_CLICKED,
|
||||
curses.BUTTON4_CLICKED,
|
||||
]
|
||||
# def __init__(self, console: "Console", target: "MessageTarget") -> None:
|
||||
# super().__init__(console, target)
|
||||
# self._stdscr = None
|
||||
# self._exit_event = Event()
|
||||
# self._key_thread: Thread | None = None
|
||||
|
||||
_MOUSE_DOUBLE_CLICKED = [
|
||||
curses.BUTTON1_DOUBLE_CLICKED,
|
||||
curses.BUTTON2_DOUBLE_CLICKED,
|
||||
curses.BUTTON3_DOUBLE_CLICKED,
|
||||
curses.BUTTON4_DOUBLE_CLICKED,
|
||||
]
|
||||
# def _get_terminal_size(self) -> tuple[int, int]:
|
||||
# width: int | None = 80
|
||||
# height: int | None = 25
|
||||
# if WINDOWS: # pragma: no cover
|
||||
# width, height = shutil.get_terminal_size()
|
||||
# else:
|
||||
# try:
|
||||
# width, height = os.get_terminal_size(sys.stdin.fileno())
|
||||
# except (AttributeError, ValueError, OSError):
|
||||
# try:
|
||||
# width, height = os.get_terminal_size(sys.stdout.fileno())
|
||||
# except (AttributeError, ValueError, OSError):
|
||||
# pass
|
||||
# width = width or 80
|
||||
# height = height or 25
|
||||
# return width, height
|
||||
|
||||
_MOUSE = [
|
||||
(events.Press, _MOUSE_PRESSED),
|
||||
(events.Release, _MOUSE_RELEASED),
|
||||
(events.Click, _MOUSE_CLICKED),
|
||||
(events.DoubleClick, _MOUSE_DOUBLE_CLICKED),
|
||||
]
|
||||
# def start_application_mode(self):
|
||||
# loop = asyncio.get_event_loop()
|
||||
|
||||
def __init__(self, console: "Console", target: "MessageTarget") -> None:
|
||||
super().__init__(console, target)
|
||||
self._stdscr = None
|
||||
self._exit_event = Event()
|
||||
self._key_thread: Thread | None = None
|
||||
# def on_terminal_resize(signum, stack) -> None:
|
||||
# terminal_size = self._get_terminal_size()
|
||||
# width, height = terminal_size
|
||||
# event = events.Resize(self._target, width, height)
|
||||
# self.console.size = terminal_size
|
||||
# asyncio.run_coroutine_threadsafe(
|
||||
# self._target.post_message(event),
|
||||
# loop=loop,
|
||||
# )
|
||||
|
||||
def _get_terminal_size(self) -> tuple[int, int]:
|
||||
width: int | None = 80
|
||||
height: int | None = 25
|
||||
if WINDOWS: # pragma: no cover
|
||||
width, height = shutil.get_terminal_size()
|
||||
else:
|
||||
try:
|
||||
width, height = os.get_terminal_size(sys.stdin.fileno())
|
||||
except (AttributeError, ValueError, OSError):
|
||||
try:
|
||||
width, height = os.get_terminal_size(sys.stdout.fileno())
|
||||
except (AttributeError, ValueError, OSError):
|
||||
pass
|
||||
width = width or 80
|
||||
height = height or 25
|
||||
return width, height
|
||||
# signal.signal(signal.SIGWINCH, on_terminal_resize)
|
||||
# self._stdscr = curses.initscr()
|
||||
# curses.noecho()
|
||||
# curses.cbreak()
|
||||
# curses.halfdelay(1)
|
||||
# curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS)
|
||||
# # curses.mousemask(-1)
|
||||
|
||||
def start_application_mode(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
# self._stdscr.keypad(True)
|
||||
# self.console.show_cursor(False)
|
||||
# self.console.file.write("\033[?1003h\n")
|
||||
# self._key_thread = Thread(
|
||||
# target=self.run_key_thread, args=(asyncio.get_event_loop(),)
|
||||
# )
|
||||
|
||||
def on_terminal_resize(signum, stack) -> None:
|
||||
terminal_size = self._get_terminal_size()
|
||||
width, height = terminal_size
|
||||
event = events.Resize(self._target, width, height)
|
||||
self.console.size = terminal_size
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._target.post_message(event),
|
||||
loop=loop,
|
||||
)
|
||||
# width, height = self.console.size = self._get_terminal_size()
|
||||
# asyncio.run_coroutine_threadsafe(
|
||||
# self._target.post_message(events.Resize(self._target, width, height)),
|
||||
# loop=loop,
|
||||
# )
|
||||
|
||||
signal.signal(signal.SIGWINCH, on_terminal_resize)
|
||||
self._stdscr = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
curses.halfdelay(1)
|
||||
curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS)
|
||||
# curses.mousemask(-1)
|
||||
# self._key_thread.start()
|
||||
|
||||
self._stdscr.keypad(True)
|
||||
self.console.show_cursor(False)
|
||||
self.console.file.write("\033[?1003h\n")
|
||||
self._key_thread = Thread(
|
||||
target=self.run_key_thread, args=(asyncio.get_event_loop(),)
|
||||
)
|
||||
# def stop_application_mode(self):
|
||||
|
||||
width, height = self.console.size = self._get_terminal_size()
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._target.post_message(events.Resize(self._target, width, height)),
|
||||
loop=loop,
|
||||
)
|
||||
# signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||
|
||||
self._key_thread.start()
|
||||
# self._exit_event.set()
|
||||
# self._key_thread.join()
|
||||
# curses.nocbreak()
|
||||
# self._stdscr.keypad(False)
|
||||
# curses.echo()
|
||||
# curses.endwin()
|
||||
# self.console.show_cursor(True)
|
||||
|
||||
def stop_application_mode(self):
|
||||
# def run_key_thread(self, loop) -> None:
|
||||
# stdscr = self._stdscr
|
||||
# assert stdscr is not None
|
||||
# exit_event = self._exit_event
|
||||
|
||||
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||
# def send_event(event: events.Event) -> None:
|
||||
# asyncio.run_coroutine_threadsafe(
|
||||
# self._target.post_message(event),
|
||||
# loop=loop,
|
||||
# )
|
||||
|
||||
self._exit_event.set()
|
||||
self._key_thread.join()
|
||||
curses.nocbreak()
|
||||
self._stdscr.keypad(False)
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
self.console.show_cursor(True)
|
||||
# while not exit_event.is_set():
|
||||
# code = stdscr.getch()
|
||||
# if code == -1:
|
||||
# continue
|
||||
|
||||
def run_key_thread(self, loop) -> None:
|
||||
stdscr = self._stdscr
|
||||
assert stdscr is not None
|
||||
exit_event = self._exit_event
|
||||
# if code == curses.KEY_MOUSE:
|
||||
|
||||
def send_event(event: events.Event) -> None:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._target.post_message(event),
|
||||
loop=loop,
|
||||
)
|
||||
|
||||
while not exit_event.is_set():
|
||||
code = stdscr.getch()
|
||||
if code == -1:
|
||||
continue
|
||||
|
||||
if code == curses.KEY_MOUSE:
|
||||
|
||||
try:
|
||||
_id, x, y, _z, button_state = curses.getmouse()
|
||||
except Exception:
|
||||
log.exception("error in curses.getmouse")
|
||||
else:
|
||||
if button_state & curses.REPORT_MOUSE_POSITION:
|
||||
send_event(events.Move(self._target, x, y))
|
||||
alt = bool(button_state & curses.BUTTON_ALT)
|
||||
ctrl = bool(button_state & curses.BUTTON_CTRL)
|
||||
shift = bool(button_state & curses.BUTTON_SHIFT)
|
||||
for event_type, masks in self._MOUSE:
|
||||
for button, mask in enumerate(masks, 1):
|
||||
if button_state & mask:
|
||||
send_event(
|
||||
event_type(
|
||||
self._target,
|
||||
x,
|
||||
y,
|
||||
button,
|
||||
alt=alt,
|
||||
ctrl=ctrl,
|
||||
shift=shift,
|
||||
)
|
||||
)
|
||||
else:
|
||||
send_event(events.Key(self._target, code=code))
|
||||
# try:
|
||||
# _id, x, y, _z, button_state = curses.getmouse()
|
||||
# except Exception:
|
||||
# log.exception("error in curses.getmouse")
|
||||
# else:
|
||||
# if button_state & curses.REPORT_MOUSE_POSITION:
|
||||
# send_event(events.MouseMove(self._target, x, y))
|
||||
# alt = bool(button_state & curses.BUTTON_ALT)
|
||||
# ctrl = bool(button_state & curses.BUTTON_CTRL)
|
||||
# shift = bool(button_state & curses.BUTTON_SHIFT)
|
||||
# for event_type, masks in self._MOUSE:
|
||||
# for button, mask in enumerate(masks, 1):
|
||||
# if button_state & mask:
|
||||
# send_event(
|
||||
# event_type(
|
||||
# self._target,
|
||||
# x,
|
||||
# y,
|
||||
# button,
|
||||
# alt=alt,
|
||||
# ctrl=ctrl,
|
||||
# shift=shift,
|
||||
# )
|
||||
# )
|
||||
# else:
|
||||
# send_event(events.Key(self._target, code=code))
|
||||
|
||||
@@ -36,9 +36,9 @@ class EventType(Enum):
|
||||
FOCUS = auto()
|
||||
BLUR = auto()
|
||||
KEY = auto()
|
||||
MOVE = auto()
|
||||
PRESS = auto()
|
||||
RELEASE = auto()
|
||||
MOUSE_MOVE = auto()
|
||||
MOUSE_DOWN = auto()
|
||||
MOUSE_UP = auto()
|
||||
CLICK = auto()
|
||||
DOUBLE_CLICK = auto()
|
||||
ENTER = auto()
|
||||
@@ -134,7 +134,7 @@ class Key(Event, type=EventType.KEY, bubble=True):
|
||||
|
||||
|
||||
@rich_repr
|
||||
class _MouseBase(Event, type=EventType.PRESS):
|
||||
class _MouseBase(Event, type=EventType.MOUSE_MOVE):
|
||||
__slots__ = ["x", "y", "button"]
|
||||
|
||||
def __init__(
|
||||
@@ -164,15 +164,15 @@ class _MouseBase(Event, type=EventType.PRESS):
|
||||
yield "ctrl", self.ctrl, False
|
||||
|
||||
|
||||
class Move(_MouseBase, type=EventType.MOVE):
|
||||
class MouseMove(_MouseBase, type=EventType.MOUSE_MOVE):
|
||||
pass
|
||||
|
||||
|
||||
class Press(_MouseBase, type=EventType.MOVE):
|
||||
class MouseDown(_MouseBase, type=EventType.MOUSE_DOWN):
|
||||
pass
|
||||
|
||||
|
||||
class Release(_MouseBase, type=EventType.RELEASE):
|
||||
class MouseUp(_MouseBase, type=EventType.MOUSE_UP):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -157,6 +157,8 @@ class MessagePump:
|
||||
|
||||
try:
|
||||
await self.dispatch_message(message, priority)
|
||||
except Exception:
|
||||
log.exception("error dispatching %r", message)
|
||||
finally:
|
||||
if self._message_queue.empty():
|
||||
idle_handler = getattr(self, "on_idle", None)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import ceil
|
||||
import logging
|
||||
from typing import Iterable
|
||||
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
|
||||
# def add_vertical_bar(lines:list[list[Segment]], size:float, window_size:float, position:float) -> None
|
||||
# bar = render_bar(len(lines), size, window_size, po)
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
|
||||
class VerticalBar:
|
||||
@@ -88,25 +88,33 @@ def render_bar(
|
||||
step_size = virtual_size / size
|
||||
|
||||
start = position / step_size
|
||||
end = (position + window_size) / step_size
|
||||
# end = (position + window_size) / step_size
|
||||
end = start + window_size / step_size
|
||||
|
||||
start_index = int(start)
|
||||
end_index = int(end)
|
||||
bar_height = (end_index - start_index) + 1
|
||||
end_index = start_index + ceil(window_size / step_size)
|
||||
bar_height = end_index - start_index
|
||||
|
||||
segments[start_index:end_index] = [bar_segment] * bar_height
|
||||
|
||||
sub_position = start % 1.0
|
||||
if sub_position >= 0.5:
|
||||
segments[start_index] = start_bar_segment
|
||||
elif start_index:
|
||||
segments[start_index - 1] = end_back_segment
|
||||
# log.debug("f")
|
||||
# sub_position = 1 - (start % 1.0)
|
||||
# log.debug("*** sub_position=%r, %r", start, sub_position)
|
||||
|
||||
sub_position = end % 1.0
|
||||
if sub_position < 0.5:
|
||||
segments[end_index] = end_bar_segment
|
||||
elif end_index + 1 < len(segments):
|
||||
segments[end_index + 1] = start_back_segment
|
||||
# if sub_position > 0.5:
|
||||
# segments[start_index - 1] = end_back_segment
|
||||
# segments[start_index] = start_back_segment
|
||||
# else:
|
||||
# segments[start_index] = start_bar_segment
|
||||
# # segments[start_index + 1] = start_bar_segment
|
||||
|
||||
# sub_position = end % 1.0
|
||||
# if sub_position > 0.5:
|
||||
# segments[end_index] = end_bar_segment
|
||||
# segments[end_index + 1] = back_segment
|
||||
# else:
|
||||
# segments[end_index] = start_back_segment
|
||||
# segments[end_index + 1] = start_back_segment
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
@@ -127,8 +127,10 @@ class LayoutView(View):
|
||||
self._widgets.add(widget)
|
||||
|
||||
async def set_focus(self, widget: Optional[Widget]) -> None:
|
||||
log.debug("set_focus %r", widget)
|
||||
if widget == self.focused:
|
||||
return
|
||||
|
||||
if widget is None:
|
||||
if self.focused is not None:
|
||||
focused = self.focused
|
||||
@@ -153,7 +155,7 @@ class LayoutView(View):
|
||||
)
|
||||
self.app.refresh()
|
||||
|
||||
async def on_move(self, event: events.Move) -> None:
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
try:
|
||||
widget, region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
@@ -174,7 +176,7 @@ class LayoutView(View):
|
||||
finally:
|
||||
self.mouse_over = widget
|
||||
await widget.post_message(
|
||||
events.Move(
|
||||
events.MouseMove(
|
||||
self,
|
||||
event.x - region.x,
|
||||
event.y - region.y,
|
||||
@@ -185,10 +187,15 @@ class LayoutView(View):
|
||||
)
|
||||
)
|
||||
|
||||
async def on_click(self, event: events.Click) -> None:
|
||||
async def on_mouse_down(self, event: events.Click) -> None:
|
||||
try:
|
||||
widget, _region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
await self.set_focus(None)
|
||||
else:
|
||||
await self.set_focus(widget)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
log.debug("view.on_key; %s, %r", event, self.focused)
|
||||
if self.focused:
|
||||
await self.focused.post_message(event)
|
||||
|
||||
@@ -71,16 +71,16 @@ class Widget(MessagePump):
|
||||
super().__init__()
|
||||
if not self.mouse_events:
|
||||
self.disable_messages(
|
||||
events.Move,
|
||||
events.Press,
|
||||
events.Release,
|
||||
events.MouseMove,
|
||||
events.MouseDown,
|
||||
events.MouseUp,
|
||||
events.Click,
|
||||
events.DoubleClick,
|
||||
)
|
||||
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
can_focus: bool = False,
|
||||
can_focus: bool = True,
|
||||
mouse_events: bool = True,
|
||||
) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
@@ -10,10 +12,13 @@ else:
|
||||
from rich.console import Console, ConsoleOptions, RenderableType
|
||||
from rich.segment import Segment
|
||||
|
||||
from .. import events
|
||||
from ..widget import Widget, Reactive
|
||||
from ..geometry import Point, Dimensions
|
||||
from ..scrollbar import VerticalBar
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
ScrollMethod = Literal["always", "never", "auto", "overlay"]
|
||||
|
||||
|
||||
@@ -66,3 +71,11 @@ class Window(Widget):
|
||||
self.position,
|
||||
overlay=self.y_scroll == "overlay",
|
||||
)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
log.debug("window.on_key; %s", event)
|
||||
if event.key == "down":
|
||||
self.position += 1
|
||||
elif event.key == "up":
|
||||
self.position -= 1
|
||||
event.stop_propagation()
|
||||
|
||||
Reference in New Issue
Block a user