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