mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
grab to scroll
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
from logging import FileHandler
|
||||
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from textual import events
|
||||
@@ -6,6 +9,14 @@ from textual.view import DockView
|
||||
from textual.widgets import Header, Footer, Placeholder, ScrollView
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level="NOTSET",
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[FileHandler("richtui.log")],
|
||||
)
|
||||
|
||||
|
||||
class MyApp(App):
|
||||
"""An example of a very simple Textual App"""
|
||||
|
||||
@@ -14,25 +25,27 @@ class MyApp(App):
|
||||
await self.bind("b", "view.toggle('sidebar')")
|
||||
|
||||
async def on_startup(self, event: events.Startup) -> None:
|
||||
|
||||
view = await self.push_view(DockView())
|
||||
header = Header(self.title)
|
||||
|
||||
footer = Footer()
|
||||
sidebar = Placeholder(name="sidebar")
|
||||
|
||||
with open("richreadme.md", "rt") as fh:
|
||||
readme = Markdown(fh.read(), hyperlinks=True)
|
||||
|
||||
body = ScrollView(readme)
|
||||
|
||||
footer.add_key("b", "Toggle sidebar")
|
||||
footer.add_key("q", "Quit")
|
||||
header = Header(self.title)
|
||||
body = ScrollView()
|
||||
sidebar = Placeholder()
|
||||
|
||||
await view.dock(header, edge="top")
|
||||
await view.dock(footer, edge="bottom")
|
||||
await view.dock(sidebar, edge="left", size=30)
|
||||
await view.dock(sidebar, edge="left", size=30, name="sidebar")
|
||||
await view.dock(body, edge="right")
|
||||
self.require_layout()
|
||||
|
||||
async def get_markdown(filename: str) -> None:
|
||||
with open(filename, "rt") as fh:
|
||||
readme = Markdown(fh.read(), hyperlinks=True)
|
||||
await body.update(readme)
|
||||
|
||||
await self.call_later(get_markdown, "richreadme.md")
|
||||
|
||||
|
||||
app = MyApp(title="Simple App")
|
||||
app.run()
|
||||
MyApp.run(title="Simple App")
|
||||
|
||||
@@ -120,12 +120,15 @@ class Animator:
|
||||
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
|
||||
self._animations: dict[tuple[object, str], Animation] = {}
|
||||
self._timer = Timer(target, 1 / frames_per_second, target, callback=self)
|
||||
self._timer_task: asyncio.Task | None = None
|
||||
|
||||
async def start(self) -> None:
|
||||
asyncio.get_event_loop().create_task(self._timer.run())
|
||||
self._timer_task = asyncio.get_event_loop().create_task(self._timer.run())
|
||||
|
||||
async def stop(self) -> None:
|
||||
self._timer.stop()
|
||||
if self._timer_task:
|
||||
await self._timer_task
|
||||
|
||||
def bind(self, obj: object) -> BoundAnimator:
|
||||
return BoundAnimator(self, obj)
|
||||
|
||||
@@ -50,8 +50,10 @@ class LinuxDriver(Driver):
|
||||
def _enable_mouse_support(self) -> None:
|
||||
write = self.console.file.write
|
||||
write("\x1b[?1000h")
|
||||
|
||||
write("\x1b[?1015h")
|
||||
write("\x1b[?1006h")
|
||||
|
||||
# write("\x1b[?1007h")
|
||||
self.console.file.flush()
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ class Timer:
|
||||
count += 1
|
||||
continue
|
||||
try:
|
||||
|
||||
if await wait_for(_wait(), max(0, next_timer - monotonic())):
|
||||
break
|
||||
except TimeoutError:
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
import os
|
||||
|
||||
import asyncio
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
import signal
|
||||
from typing import Any, ClassVar, Type, TypeVar
|
||||
from typing import Any, Callable, ClassVar, Type, TypeVar
|
||||
import warnings
|
||||
|
||||
from rich.control import Control
|
||||
@@ -78,7 +78,7 @@ class App(MessagePump):
|
||||
self.driver_class = driver_class or LinuxDriver
|
||||
self.title = title
|
||||
self._layout = DockLayout()
|
||||
self._view_stack: list[View] = [View()]
|
||||
self._view_stack: list[View] = []
|
||||
self.children: set[MessagePump] = set()
|
||||
|
||||
self.focused: Widget | None = None
|
||||
@@ -117,7 +117,11 @@ class App(MessagePump):
|
||||
|
||||
@classmethod
|
||||
def run(
|
||||
cls, console: Console = None, screen: bool = True, driver: Type[Driver] = None
|
||||
cls,
|
||||
console: Console = None,
|
||||
screen: bool = True,
|
||||
driver: Type[Driver] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run the app.
|
||||
|
||||
@@ -128,8 +132,7 @@ class App(MessagePump):
|
||||
"""
|
||||
|
||||
async def run_app() -> None:
|
||||
app = cls(console=console, screen=screen, driver_class=driver)
|
||||
|
||||
app = cls(console=console, screen=screen, driver_class=driver, **kwargs)
|
||||
await app.process_messages()
|
||||
|
||||
asyncio.run(run_app())
|
||||
@@ -137,7 +140,7 @@ class App(MessagePump):
|
||||
async def push_view(self, view: ViewType) -> ViewType:
|
||||
await self.register(view)
|
||||
view.set_parent(self)
|
||||
self._view_stack[0] = view
|
||||
self._view_stack.append(view)
|
||||
return view
|
||||
|
||||
def on_keyboard_interupt(self) -> None:
|
||||
@@ -181,7 +184,13 @@ class App(MessagePump):
|
||||
|
||||
async def capture_mouse(self, widget: Widget | None) -> None:
|
||||
"""Send all Mouse events to a given widget."""
|
||||
if widget == self.mouse_captured:
|
||||
return
|
||||
if self.mouse_captured is not None:
|
||||
await self.mouse_captured.post_message(events.MouseReleased(self))
|
||||
self.mouse_captured = widget
|
||||
if widget is not None:
|
||||
await widget.post_message(events.MouseCaptured(self))
|
||||
|
||||
async def process_messages(self) -> None:
|
||||
log.debug("driver=%r", self.driver_class)
|
||||
@@ -189,16 +198,18 @@ class App(MessagePump):
|
||||
# loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt)
|
||||
driver = self._driver = self.driver_class(self.console, self)
|
||||
active_app.set(self)
|
||||
|
||||
await self.push_view(View())
|
||||
|
||||
self.view.set_parent(self)
|
||||
await self.register(self.view)
|
||||
|
||||
if hasattr(self, "on_load"):
|
||||
await self.on_load(events.Load(sender=self))
|
||||
|
||||
await self.post_message(events.Startup(sender=self))
|
||||
await self.dispatch_message(events.Load(sender=self))
|
||||
|
||||
try:
|
||||
driver.start_application_mode()
|
||||
await self.post_message(events.Startup(sender=self))
|
||||
self.require_layout()
|
||||
except Exception:
|
||||
self.console.print_exception()
|
||||
log.exception("error starting application mode")
|
||||
@@ -207,11 +218,19 @@ class App(MessagePump):
|
||||
await self.animator.start()
|
||||
try:
|
||||
await super().process_messages()
|
||||
await self.animator.stop()
|
||||
|
||||
while self.children:
|
||||
child = self.children.pop()
|
||||
log.debug("closing %r", child)
|
||||
await child.close_messages()
|
||||
|
||||
while self._view_stack:
|
||||
view = self._view_stack.pop()
|
||||
await view.close_messages()
|
||||
except Exception:
|
||||
traceback = Traceback(show_locals=True)
|
||||
|
||||
await self.animator.stop()
|
||||
await self.view.close_messages()
|
||||
driver.stop_application_mode()
|
||||
if traceback is not None:
|
||||
self.console.print(traceback)
|
||||
@@ -222,6 +241,12 @@ class App(MessagePump):
|
||||
def require_layout(self) -> None:
|
||||
self.view.require_layout()
|
||||
|
||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
await self.post_message(events.Idle(self))
|
||||
await self.post_message(
|
||||
events.Callback(self, partial(callback, *args, **kwargs))
|
||||
)
|
||||
|
||||
async def message_update(self, message: Message) -> None:
|
||||
self.refresh()
|
||||
|
||||
@@ -308,8 +333,8 @@ class App(MessagePump):
|
||||
if method is not None:
|
||||
await method(*params)
|
||||
|
||||
async def on_load(self, event: events.Load) -> None:
|
||||
pass
|
||||
async def on_callback(self, event: events.Callback) -> None:
|
||||
await event.callback()
|
||||
|
||||
async def on_shutdown_request(self, event: events.ShutdownRequest) -> None:
|
||||
log.debug("shutdown request")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
from rich.repr import rich_repr, RichReprResult
|
||||
from rich.style import Style
|
||||
|
||||
from .message import Message
|
||||
from ._types import MessageTarget
|
||||
@@ -29,6 +30,13 @@ class Null(Event):
|
||||
return isinstance(message, Null)
|
||||
|
||||
|
||||
@rich_repr
|
||||
class Callback(Event):
|
||||
def __init__(self, sender: MessageTarget, callback: Callable[[], None]) -> None:
|
||||
self.callback = callback
|
||||
super().__init__(sender)
|
||||
|
||||
|
||||
class ShutdownRequest(Event):
|
||||
pass
|
||||
|
||||
@@ -99,6 +107,14 @@ class Hide(Event):
|
||||
"""Widget has been hidden."""
|
||||
|
||||
|
||||
class MouseCaptured(Event):
|
||||
"""Mouse has been captured."""
|
||||
|
||||
|
||||
class MouseReleased(Event):
|
||||
"""Mouse has been released."""
|
||||
|
||||
|
||||
class InputEvent(Event, bubble=True):
|
||||
pass
|
||||
|
||||
@@ -132,6 +148,7 @@ class MouseEvent(InputEvent):
|
||||
ctrl: bool,
|
||||
screen_x: int | None = None,
|
||||
screen_y: int | None = None,
|
||||
style: Style | None = None,
|
||||
) -> None:
|
||||
super().__init__(sender)
|
||||
self.x = x
|
||||
@@ -144,6 +161,7 @@ class MouseEvent(InputEvent):
|
||||
self.ctrl = ctrl
|
||||
self.screen_x = x if screen_x is None else screen_x
|
||||
self.screen_y = y if screen_y is None else screen_y
|
||||
self._style = style or Style()
|
||||
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
yield "x", self.x
|
||||
@@ -158,6 +176,15 @@ class MouseEvent(InputEvent):
|
||||
yield "shift", self.shift, False
|
||||
yield "meta", self.meta, False
|
||||
yield "ctrl", self.ctrl, False
|
||||
yield "style", self._style, None
|
||||
|
||||
@property
|
||||
def style(self) -> Style:
|
||||
return self._style or Style()
|
||||
|
||||
@style.setter
|
||||
def style(self, style: Style) -> None:
|
||||
self._style = style
|
||||
|
||||
def offset(self, x: int, y: int):
|
||||
return self.__class__(
|
||||
@@ -172,6 +199,7 @@ class MouseEvent(InputEvent):
|
||||
ctrl=self.ctrl,
|
||||
screen_x=self.screen_x,
|
||||
screen_y=self.screen_y,
|
||||
style=self.style,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -164,6 +164,14 @@ class Layout(ABC):
|
||||
return segment.style or Style.null()
|
||||
return Style.null()
|
||||
|
||||
def get_widget_region(self, widget: Widget) -> Region:
|
||||
try:
|
||||
region, _ = self._layout_map[widget]
|
||||
except KeyError:
|
||||
raise NoWidget("Widget is not in layout")
|
||||
else:
|
||||
return region
|
||||
|
||||
@property
|
||||
def cuts(self) -> list[list[int]]:
|
||||
if self._cuts is not None:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Coroutine, Awaitable, NamedTuple
|
||||
import asyncio
|
||||
from asyncio import Event, Queue, Task, QueueEmpty
|
||||
|
||||
import logging
|
||||
from asyncio import Event, Queue, QueueEmpty, Task
|
||||
from typing import Any, Awaitable, Coroutine, NamedTuple
|
||||
from weakref import WeakSet
|
||||
|
||||
from . import events
|
||||
from .message import Message
|
||||
from ._timer import Timer, TimerCallback
|
||||
from ._types import MessageHandler
|
||||
from .message import Message
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
@@ -31,7 +31,7 @@ class MessagePump:
|
||||
self._disabled_messages: set[type[Message]] = set()
|
||||
self._pending_message: Message | None = None
|
||||
self._task: Task | None = None
|
||||
self._child_tasks: set[Task] = set()
|
||||
self._child_tasks: WeakSet[Task] = WeakSet()
|
||||
|
||||
@property
|
||||
def task(self) -> Task:
|
||||
@@ -117,7 +117,7 @@ class MessagePump:
|
||||
timer = Timer(
|
||||
self, interval, self, name=name, callback=callback, repeat=repeat or None
|
||||
)
|
||||
asyncio.get_event_loop().create_task(timer.run())
|
||||
self._child_tasks.add(asyncio.get_event_loop().create_task(timer.run()))
|
||||
return timer
|
||||
|
||||
async def close_messages(self, wait: bool = True) -> None:
|
||||
@@ -126,10 +126,13 @@ class MessagePump:
|
||||
return
|
||||
|
||||
self._closing = True
|
||||
|
||||
await self._message_queue.put(None)
|
||||
|
||||
for task in self._child_tasks:
|
||||
task.cancel()
|
||||
await task
|
||||
self._child_tasks.clear()
|
||||
|
||||
def start_messages(self) -> None:
|
||||
self._task = asyncio.create_task(self.process_messages())
|
||||
@@ -167,6 +170,7 @@ class MessagePump:
|
||||
idle_handler = getattr(self, "on_idle", None)
|
||||
if idle_handler is not None and not self._closed:
|
||||
await idle_handler(events.Idle(self))
|
||||
|
||||
log.debug("CLOSED %r", self)
|
||||
|
||||
async def dispatch_message(self, message: Message) -> bool | None:
|
||||
|
||||
@@ -3,9 +3,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from rich.repr import rich_repr, RichReprResult
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment, Segments
|
||||
from rich.style import Style, StyleType
|
||||
@@ -13,16 +12,35 @@ from rich.style import Style, StyleType
|
||||
log = logging.getLogger("rich")
|
||||
|
||||
from . import events
|
||||
from ._types import MessageTarget
|
||||
from .message import Message
|
||||
from .widget import Reactive, Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class ScrollUp(Message, bubble=True):
|
||||
pass
|
||||
"""Message sent when clicking above handle."""
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class ScrollDown(Message, bubble=True):
|
||||
pass
|
||||
"""Message sent when clicking below handle."""
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class ScrollRelative(Message, bubble=True):
|
||||
"""Message sent when click and dragging handle."""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, x: float | None = None, y: float | None = None
|
||||
) -> None:
|
||||
self.x = x
|
||||
self.y = y
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
||||
yield "x", self.x, None
|
||||
yield "y", self.y, None
|
||||
|
||||
|
||||
class ScrollBarRender:
|
||||
@@ -76,7 +94,7 @@ class ScrollBarRender:
|
||||
_Style = Style
|
||||
blank = " " * width_thickness
|
||||
|
||||
foreground_meta = {"background": False}
|
||||
foreground_meta = {"@click": "release", "@mouse.down": "grab"}
|
||||
|
||||
if window_size and size and virtual_size:
|
||||
step_size = virtual_size / size
|
||||
@@ -150,7 +168,7 @@ class ScrollBarRender:
|
||||
yield bar
|
||||
|
||||
|
||||
@rich_repr
|
||||
@rich.repr.auto
|
||||
class ScrollBar(Widget):
|
||||
def __init__(self, vertical: bool = True, name: str | None = None) -> None:
|
||||
self.vertical = vertical
|
||||
@@ -160,21 +178,24 @@ class ScrollBar(Widget):
|
||||
window_size: Reactive[int] = Reactive(20)
|
||||
position: Reactive[int] = Reactive(0)
|
||||
mouse_over: Reactive[bool] = Reactive(False)
|
||||
grabbed: Reactive[bool] = Reactive(False)
|
||||
|
||||
def __rich_repr__(self) -> RichReprResult:
|
||||
def __rich_repr__(self) -> rich.repr.RichReprResult:
|
||||
yield "virtual_size", self.virtual_size
|
||||
yield "window_size", self.window_size
|
||||
yield "position", self.position
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
style = Style(
|
||||
bgcolor=(Color.parse("#555555" if self.mouse_over else "#444444")),
|
||||
color=Color.parse("bright_yellow" if self.grabbed else "bright_magenta"),
|
||||
)
|
||||
return ScrollBarRender(
|
||||
virtual_size=self.virtual_size,
|
||||
window_size=self.window_size,
|
||||
position=self.position,
|
||||
vertical=self.vertical,
|
||||
style="bright_magenta on #555555"
|
||||
if self.mouse_over
|
||||
else "bright_magenta on #444444",
|
||||
style=style,
|
||||
)
|
||||
|
||||
async def on_enter(self, event: events.Enter) -> None:
|
||||
@@ -189,6 +210,28 @@ class ScrollBar(Widget):
|
||||
async def action_scroll_up(self) -> None:
|
||||
await self.emit(ScrollUp(self))
|
||||
|
||||
async def action_grab(self) -> None:
|
||||
await self.capture_mouse()
|
||||
|
||||
async def action_released(self) -> None:
|
||||
await self.capture_mouse(False)
|
||||
|
||||
async def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
if self.grabbed:
|
||||
await self.release_mouse()
|
||||
await super().on_mouse_up(event)
|
||||
|
||||
async def on_mouse_captured(self, event: events.MouseCaptured) -> None:
|
||||
self.grabbed = True
|
||||
|
||||
async def on_mouse_released(self, event: events.MouseReleased) -> None:
|
||||
self.grabbed = False
|
||||
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
if self.grabbed:
|
||||
delta_y = event.delta_y * (self.virtual_size / self.size.height)
|
||||
await self.emit(ScrollRelative(self, y=delta_y))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich.console import Console
|
||||
|
||||
@@ -121,8 +121,15 @@ class View(Widget):
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
return self.layout.get_style_at(x, y)
|
||||
|
||||
def get_widget_region(self, widget: Widget) -> Region:
|
||||
return self.layout.get_widget_region(widget)
|
||||
|
||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
try:
|
||||
if self.app.mouse_captured:
|
||||
widget = self.app.mouse_captured
|
||||
region = self.get_widget_region(widget)
|
||||
else:
|
||||
widget, region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
await self.app.set_mouse_over(None)
|
||||
@@ -140,6 +147,9 @@ class View(Widget):
|
||||
event.shift,
|
||||
event.meta,
|
||||
event.ctrl,
|
||||
screen_x=event.screen_x,
|
||||
screen_y=event.screen_y,
|
||||
style=event.style,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -149,17 +159,22 @@ class View(Widget):
|
||||
await self.post_message(event)
|
||||
|
||||
elif isinstance(event, events.MouseMove):
|
||||
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||
await self._on_mouse_move(event)
|
||||
|
||||
elif isinstance(event, events.MouseEvent):
|
||||
|
||||
try:
|
||||
if self.app.mouse_captured:
|
||||
widget = self.app.mouse_captured
|
||||
region = self.get_widget_region(widget)
|
||||
else:
|
||||
widget, region = self.get_widget_at(event.x, event.y)
|
||||
except NoWidget:
|
||||
pass
|
||||
else:
|
||||
if isinstance(event, events.MouseDown) and widget.can_focus:
|
||||
await self.app.set_focus(widget)
|
||||
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
||||
await widget.forward_event(event.offset(-region.x, -region.y))
|
||||
|
||||
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
|
||||
@@ -199,7 +214,7 @@ class DockView(View):
|
||||
z: int = 0,
|
||||
size: int | None | DoNotSet = do_not_set,
|
||||
name: str | None = None
|
||||
) -> Widget | tuple[Widget, ...]:
|
||||
) -> None:
|
||||
|
||||
dock = Dock(edge, widgets, z)
|
||||
assert isinstance(self.layout, DockLayout)
|
||||
@@ -208,11 +223,8 @@ class DockView(View):
|
||||
if size is not do_not_set:
|
||||
widget.layout_size = cast(Optional[int], size)
|
||||
if not self.is_mounted(widget):
|
||||
if name is None:
|
||||
await self.mount(widget)
|
||||
await self.refresh_layout()
|
||||
|
||||
widget, *rest = widgets
|
||||
if rest:
|
||||
return widgets
|
||||
else:
|
||||
return widget
|
||||
await self.mount(**{name: widget})
|
||||
await self.refresh_layout()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from logging import getLogger
|
||||
from typing import (
|
||||
Callable,
|
||||
@@ -143,6 +144,9 @@ class Widget(MessagePump):
|
||||
offset_x, offset_y = self.root_view.get_offset(self)
|
||||
return self.root_view.get_style_at(x + offset_x, y + offset_y)
|
||||
|
||||
async def call_later(self, callback: Callable, *args, **kwargs) -> None:
|
||||
await self.app.call_later(callback, *args, **kwargs)
|
||||
|
||||
async def forward_event(self, event: events.Event) -> None:
|
||||
await self.post_message(event)
|
||||
|
||||
@@ -200,12 +204,15 @@ class Widget(MessagePump):
|
||||
async def capture_mouse(self, capture: bool = True) -> None:
|
||||
await self.app.capture_mouse(self if capture else None)
|
||||
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
style_under_cursor = self.get_style_at(event.x, event.y)
|
||||
log.debug("%r", style_under_cursor)
|
||||
async def release_mouse(self) -> None:
|
||||
await self.app.capture_mouse(None)
|
||||
|
||||
async def on_mouse_down(self, event: events.MouseUp) -> None:
|
||||
if "@mouse.down" in event.style.meta:
|
||||
await self.app.action(
|
||||
event.style.meta["@mouse.down"], default_namespace=self
|
||||
)
|
||||
|
||||
async def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
style = self.get_style_at(event.x, event.y)
|
||||
if "@click" in style.meta:
|
||||
log.debug(style._link_id)
|
||||
await self.app.action(style.meta["@click"], default_namespace=self)
|
||||
if "@click" in event.style.meta:
|
||||
await self.app.action(event.style.meta["@click"], default_namespace=self)
|
||||
|
||||
@@ -8,7 +8,7 @@ from rich.style import StyleType
|
||||
|
||||
from .. import events
|
||||
from ..message import Message
|
||||
from ..scrollbar import ScrollBar, ScrollDown, ScrollUp
|
||||
from ..scrollbar import ScrollBar, ScrollDown, ScrollUp, ScrollRelative
|
||||
from ..geometry import clamp
|
||||
from ..page import Page
|
||||
from ..view import DockView
|
||||
@@ -21,7 +21,7 @@ log = logging.getLogger("rich")
|
||||
class ScrollView(DockView):
|
||||
def __init__(
|
||||
self,
|
||||
renderable: RenderableType,
|
||||
renderable: RenderableType | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
style: StyleType = "",
|
||||
@@ -29,7 +29,7 @@ class ScrollView(DockView):
|
||||
) -> None:
|
||||
self.fluid = fluid
|
||||
self._vertical_scrollbar = ScrollBar(vertical=True)
|
||||
self._page = Page(renderable, style=style)
|
||||
self._page = Page(renderable or "", style=style)
|
||||
super().__init__(name="ScrollView")
|
||||
|
||||
x: Reactive[float] = Reactive(0)
|
||||
@@ -47,6 +47,10 @@ class ScrollView(DockView):
|
||||
self._page.y = round(new_value)
|
||||
self._vertical_scrollbar.position = round(new_value)
|
||||
|
||||
async def update(self, renderabe: RenderableType) -> None:
|
||||
self._page.update(renderabe)
|
||||
self.require_repaint()
|
||||
|
||||
async def on_mount(self, event: events.Mount) -> None:
|
||||
await self.dock(self._vertical_scrollbar, edge="right", size=1)
|
||||
await self.dock(self._page, edge="top")
|
||||
@@ -105,10 +109,15 @@ class ScrollView(DockView):
|
||||
self._page.update()
|
||||
await super().on_resize(event)
|
||||
|
||||
async def on_message(self, message: Message) -> None:
|
||||
if isinstance(message, ScrollUp):
|
||||
async def message_scroll_up(self, message: Message) -> None:
|
||||
self.page_up()
|
||||
elif isinstance(message, ScrollDown):
|
||||
|
||||
async def message_scroll_down(self, message: Message) -> None:
|
||||
self.page_down()
|
||||
else:
|
||||
await super().on_message(message)
|
||||
|
||||
async def message_scroll_relative(self, message: ScrollRelative) -> None:
|
||||
if message.x is not None:
|
||||
self.target_x += message.x
|
||||
if message.y is not None:
|
||||
self.target_y += message.y
|
||||
self.animate("y", self.target_y, speed=100, easing="out_cubic")
|
||||
|
||||
Reference in New Issue
Block a user