scrolling

This commit is contained in:
Will McGugan
2021-06-13 12:58:25 +01:00
parent 77d585652d
commit c3d0020fed
11 changed files with 204 additions and 178 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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()

View File

@@ -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()

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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__()

View File

@@ -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()