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) selector_events = selector.select(0.1)
for _selector_key, mask in selector_events: for _selector_key, mask in selector_events:
unicode_data = decode(read(fileno, 1024)) unicode_data = decode(read(fileno, 1024))
log.debug(repr(unicode_data))
for event in parser.feed(unicode_data): for event in parser.feed(unicode_data):
send_event(event) send_event(event)

View File

@@ -97,11 +97,12 @@ class Parser(Generic[T]):
pos = 0 pos = 0
tokens = self._tokens tokens = self._tokens
popleft = tokens.popleft popleft = tokens.popleft
data_size = len(data)
while tokens: while tokens:
yield popleft() yield popleft()
while pos < len(data) or isinstance(self._awaiting, PeekBuffer): while pos < data_size or isinstance(self._awaiting, PeekBuffer):
_awaiting = self._awaiting _awaiting = self._awaiting
if isinstance(_awaiting, _Read1): if isinstance(_awaiting, _Read1):

View File

@@ -35,9 +35,9 @@ class XTermParser(Parser[events.Event]):
event_class: Type[events._MouseBase] event_class: Type[events._MouseBase]
if buttons & 32: if buttons & 32:
event_class = events.Move event_class = events.MouseMove
else: 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) button = (4 if (buttons & 64) else 1) + (buttons & 3)
event = event_class( event = event_class(
sender, sender,
@@ -60,7 +60,7 @@ class XTermParser(Parser[events.Event]):
while not self.is_eof: while not self.is_eof:
character = yield read1() 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 sequence: str = character
while True: while True:
sequence += yield read1() sequence += yield read1()

View File

@@ -126,6 +126,8 @@ class App(MessagePump):
if key_action is not None: if key_action is not None:
log.debug("action %r", key_action) log.debug("action %r", key_action)
await self.action(key_action) await self.action(key_action)
else:
await self.view.post_message(event)
# if event.key == "q": # if event.key == "q":
# await self.close_messages() # await self.close_messages()
@@ -137,10 +139,13 @@ class App(MessagePump):
async def on_resize(self, event: events.Resize) -> None: async def on_resize(self, event: events.Resize) -> None:
await self.view.post_message(event) 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) 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) await self.view.post_message(event)
async def action_quit(self, tokens: list[str]) -> None: async def action_quit(self, tokens: list[str]) -> None:
@@ -165,7 +170,7 @@ if __name__ == "__main__":
) )
with open("richreadme.md", "rt") as fh: 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 from rich import print
@@ -177,6 +182,5 @@ if __name__ == "__main__":
await self.view.mount_all( await self.view.mount_all(
header=Header(self.title), left=Placeholder(), body=Window(readme) header=Header(self.title), left=Placeholder(), body=Window(readme)
) )
# self.set_timer(3.0, callback=self.close_messages)
MyApp.run() MyApp.run()

View File

@@ -38,163 +38,155 @@ class Driver(ABC):
... ...
class LinuxDriver(Driver): # class CursesDriver(Driver):
def start_application_mode(self):
pass
def stop_application_mode(self): # _MOUSE_PRESSED = [
pass # 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 = [ # _MOUSE_DOUBLE_CLICKED = [
curses.BUTTON1_PRESSED, # curses.BUTTON1_DOUBLE_CLICKED,
curses.BUTTON2_PRESSED, # curses.BUTTON2_DOUBLE_CLICKED,
curses.BUTTON3_PRESSED, # curses.BUTTON3_DOUBLE_CLICKED,
curses.BUTTON4_PRESSED, # curses.BUTTON4_DOUBLE_CLICKED,
] # ]
_MOUSE_RELEASED = [ # _MOUSE = [
curses.BUTTON1_RELEASED, # (events.MouseDown, _MOUSE_PRESSED),
curses.BUTTON2_RELEASED, # (events.MouseUp, _MOUSE_RELEASED),
curses.BUTTON3_RELEASED, # (events.Click, _MOUSE_CLICKED),
curses.BUTTON4_RELEASED, # (events.DoubleClick, _MOUSE_DOUBLE_CLICKED),
] # ]
_MOUSE_CLICKED = [ # def __init__(self, console: "Console", target: "MessageTarget") -> None:
curses.BUTTON1_CLICKED, # super().__init__(console, target)
curses.BUTTON2_CLICKED, # self._stdscr = None
curses.BUTTON3_CLICKED, # self._exit_event = Event()
curses.BUTTON4_CLICKED, # self._key_thread: Thread | None = None
]
_MOUSE_DOUBLE_CLICKED = [ # def _get_terminal_size(self) -> tuple[int, int]:
curses.BUTTON1_DOUBLE_CLICKED, # width: int | None = 80
curses.BUTTON2_DOUBLE_CLICKED, # height: int | None = 25
curses.BUTTON3_DOUBLE_CLICKED, # if WINDOWS: # pragma: no cover
curses.BUTTON4_DOUBLE_CLICKED, # 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 = [ # def start_application_mode(self):
(events.Press, _MOUSE_PRESSED), # loop = asyncio.get_event_loop()
(events.Release, _MOUSE_RELEASED),
(events.Click, _MOUSE_CLICKED),
(events.DoubleClick, _MOUSE_DOUBLE_CLICKED),
]
def __init__(self, console: "Console", target: "MessageTarget") -> None: # def on_terminal_resize(signum, stack) -> None:
super().__init__(console, target) # terminal_size = self._get_terminal_size()
self._stdscr = None # width, height = terminal_size
self._exit_event = Event() # event = events.Resize(self._target, width, height)
self._key_thread: Thread | None = None # self.console.size = terminal_size
# asyncio.run_coroutine_threadsafe(
# self._target.post_message(event),
# loop=loop,
# )
def _get_terminal_size(self) -> tuple[int, int]: # signal.signal(signal.SIGWINCH, on_terminal_resize)
width: int | None = 80 # self._stdscr = curses.initscr()
height: int | None = 25 # curses.noecho()
if WINDOWS: # pragma: no cover # curses.cbreak()
width, height = shutil.get_terminal_size() # curses.halfdelay(1)
else: # curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS)
try: # # curses.mousemask(-1)
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
def start_application_mode(self): # self._stdscr.keypad(True)
loop = asyncio.get_event_loop() # 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: # width, height = self.console.size = self._get_terminal_size()
terminal_size = self._get_terminal_size() # asyncio.run_coroutine_threadsafe(
width, height = terminal_size # self._target.post_message(events.Resize(self._target, width, height)),
event = events.Resize(self._target, width, height) # loop=loop,
self.console.size = terminal_size # )
asyncio.run_coroutine_threadsafe(
self._target.post_message(event),
loop=loop,
)
signal.signal(signal.SIGWINCH, on_terminal_resize) # self._key_thread.start()
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._stdscr.keypad(True) # def stop_application_mode(self):
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(),)
)
width, height = self.console.size = self._get_terminal_size() # signal.signal(signal.SIGWINCH, signal.SIG_DFL)
asyncio.run_coroutine_threadsafe(
self._target.post_message(events.Resize(self._target, width, height)),
loop=loop,
)
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() # while not exit_event.is_set():
self._key_thread.join() # code = stdscr.getch()
curses.nocbreak() # if code == -1:
self._stdscr.keypad(False) # continue
curses.echo()
curses.endwin()
self.console.show_cursor(True)
def run_key_thread(self, loop) -> None: # if code == curses.KEY_MOUSE:
stdscr = self._stdscr
assert stdscr is not None
exit_event = self._exit_event
def send_event(event: events.Event) -> None: # try:
asyncio.run_coroutine_threadsafe( # _id, x, y, _z, button_state = curses.getmouse()
self._target.post_message(event), # except Exception:
loop=loop, # log.exception("error in curses.getmouse")
) # else:
# if button_state & curses.REPORT_MOUSE_POSITION:
while not exit_event.is_set(): # send_event(events.MouseMove(self._target, x, y))
code = stdscr.getch() # alt = bool(button_state & curses.BUTTON_ALT)
if code == -1: # ctrl = bool(button_state & curses.BUTTON_CTRL)
continue # shift = bool(button_state & curses.BUTTON_SHIFT)
# for event_type, masks in self._MOUSE:
if code == curses.KEY_MOUSE: # for button, mask in enumerate(masks, 1):
# if button_state & mask:
try: # send_event(
_id, x, y, _z, button_state = curses.getmouse() # event_type(
except Exception: # self._target,
log.exception("error in curses.getmouse") # x,
else: # y,
if button_state & curses.REPORT_MOUSE_POSITION: # button,
send_event(events.Move(self._target, x, y)) # alt=alt,
alt = bool(button_state & curses.BUTTON_ALT) # ctrl=ctrl,
ctrl = bool(button_state & curses.BUTTON_CTRL) # shift=shift,
shift = bool(button_state & curses.BUTTON_SHIFT) # )
for event_type, masks in self._MOUSE: # )
for button, mask in enumerate(masks, 1): # else:
if button_state & mask: # send_event(events.Key(self._target, code=code))
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() FOCUS = auto()
BLUR = auto() BLUR = auto()
KEY = auto() KEY = auto()
MOVE = auto() MOUSE_MOVE = auto()
PRESS = auto() MOUSE_DOWN = auto()
RELEASE = auto() MOUSE_UP = auto()
CLICK = auto() CLICK = auto()
DOUBLE_CLICK = auto() DOUBLE_CLICK = auto()
ENTER = auto() ENTER = auto()
@@ -134,7 +134,7 @@ class Key(Event, type=EventType.KEY, bubble=True):
@rich_repr @rich_repr
class _MouseBase(Event, type=EventType.PRESS): class _MouseBase(Event, type=EventType.MOUSE_MOVE):
__slots__ = ["x", "y", "button"] __slots__ = ["x", "y", "button"]
def __init__( def __init__(
@@ -164,15 +164,15 @@ class _MouseBase(Event, type=EventType.PRESS):
yield "ctrl", self.ctrl, False yield "ctrl", self.ctrl, False
class Move(_MouseBase, type=EventType.MOVE): class MouseMove(_MouseBase, type=EventType.MOUSE_MOVE):
pass pass
class Press(_MouseBase, type=EventType.MOVE): class MouseDown(_MouseBase, type=EventType.MOUSE_DOWN):
pass pass
class Release(_MouseBase, type=EventType.RELEASE): class MouseUp(_MouseBase, type=EventType.MOUSE_UP):
pass pass

View File

@@ -157,6 +157,8 @@ class MessagePump:
try: try:
await self.dispatch_message(message, priority) await self.dispatch_message(message, priority)
except Exception:
log.exception("error dispatching %r", message)
finally: finally:
if self._message_queue.empty(): if self._message_queue.empty():
idle_handler = getattr(self, "on_idle", None) idle_handler = getattr(self, "on_idle", None)

View File

@@ -1,14 +1,14 @@
from __future__ import annotations from __future__ import annotations
from math import ceil
import logging
from typing import Iterable from typing import Iterable
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
log = logging.getLogger("rich")
# 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)
class VerticalBar: class VerticalBar:
@@ -88,25 +88,33 @@ def render_bar(
step_size = virtual_size / size step_size = virtual_size / size
start = position / step_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) start_index = int(start)
end_index = int(end) end_index = start_index + ceil(window_size / step_size)
bar_height = (end_index - start_index) + 1 bar_height = end_index - start_index
segments[start_index:end_index] = [bar_segment] * bar_height segments[start_index:end_index] = [bar_segment] * bar_height
sub_position = start % 1.0 # log.debug("f")
if sub_position >= 0.5: # sub_position = 1 - (start % 1.0)
segments[start_index] = start_bar_segment # log.debug("*** sub_position=%r, %r", start, sub_position)
elif start_index:
segments[start_index - 1] = end_back_segment
sub_position = end % 1.0 # if sub_position > 0.5:
if sub_position < 0.5: # segments[start_index - 1] = end_back_segment
segments[end_index] = end_bar_segment # segments[start_index] = start_back_segment
elif end_index + 1 < len(segments): # else:
segments[end_index + 1] = start_back_segment # 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 return segments

View File

@@ -127,8 +127,10 @@ class LayoutView(View):
self._widgets.add(widget) self._widgets.add(widget)
async def set_focus(self, widget: Optional[Widget]) -> None: async def set_focus(self, widget: Optional[Widget]) -> None:
log.debug("set_focus %r", widget)
if widget == self.focused: if widget == self.focused:
return return
if widget is None: if widget is None:
if self.focused is not None: if self.focused is not None:
focused = self.focused focused = self.focused
@@ -153,7 +155,7 @@ class LayoutView(View):
) )
self.app.refresh() self.app.refresh()
async def on_move(self, event: events.Move) -> None: async def on_mouse_move(self, event: events.MouseMove) -> None:
try: try:
widget, region = self.get_widget_at(event.x, event.y) widget, region = self.get_widget_at(event.x, event.y)
except NoWidget: except NoWidget:
@@ -174,7 +176,7 @@ class LayoutView(View):
finally: finally:
self.mouse_over = widget self.mouse_over = widget
await widget.post_message( await widget.post_message(
events.Move( events.MouseMove(
self, self,
event.x - region.x, event.x - region.x,
event.y - region.y, 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: try:
widget, _region = self.get_widget_at(event.x, event.y) widget, _region = self.get_widget_at(event.x, event.y)
except NoWidget: except NoWidget:
await self.set_focus(None) await self.set_focus(None)
else: else:
await self.set_focus(widget) 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__() super().__init__()
if not self.mouse_events: if not self.mouse_events:
self.disable_messages( self.disable_messages(
events.Move, events.MouseMove,
events.Press, events.MouseDown,
events.Release, events.MouseUp,
events.Click, events.Click,
events.DoubleClick, events.DoubleClick,
) )
def __init_subclass__( def __init_subclass__(
cls, cls,
can_focus: bool = False, can_focus: bool = True,
mouse_events: bool = True, mouse_events: bool = True,
) -> None: ) -> None:
super().__init_subclass__() super().__init_subclass__()

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging
import logging
import sys import sys
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@@ -10,10 +12,13 @@ else:
from rich.console import Console, ConsoleOptions, RenderableType from rich.console import Console, ConsoleOptions, RenderableType
from rich.segment import Segment from rich.segment import Segment
from .. import events
from ..widget import Widget, Reactive from ..widget import Widget, Reactive
from ..geometry import Point, Dimensions from ..geometry import Point, Dimensions
from ..scrollbar import VerticalBar from ..scrollbar import VerticalBar
log = logging.getLogger("rich")
ScrollMethod = Literal["always", "never", "auto", "overlay"] ScrollMethod = Literal["always", "never", "auto", "overlay"]
@@ -66,3 +71,11 @@ class Window(Widget):
self.position, self.position,
overlay=self.y_scroll == "overlay", 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()