line cache

This commit is contained in:
Will McGugan
2021-06-05 22:08:16 +01:00
parent fbee25f81e
commit 91b7efa4a9
8 changed files with 80 additions and 38 deletions

View File

@@ -1,7 +1,7 @@
[tool.poetry]
name = "textual"
version = "0.1.0"
description = "Rich TUI"
description = "Text User Interface using Rich"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"

View File

@@ -2,6 +2,9 @@
"folders": [
{
"path": "."
},
{
"path": "../rich"
}
],
"settings": {

View File

@@ -0,0 +1,39 @@
from typing import Iterable, List
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
from rich.control import Control
from rich.segment import Segment
class LineCache:
def __init__(self, lines: List[List[Segment]]) -> None:
self.lines = lines
self.dirty = [True] * len(self.lines)
@classmethod
def from_renderable(
cls,
console: Console,
renderable: RenderableType,
width: int,
height: int,
) -> "LineCache":
options = console.options.update_dimensions(width, height)
lines = console.render_lines(renderable, options, new_lines=False)
return cls(lines)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
new_line = Segment.line()
for line in self.lines:
yield from line
yield new_line
def render(self, x: int, y: int) -> Iterable[Segment]:
move_to = Control.move_to
for offset_y, (line, dirty) in enumerate(zip(self.lines, self.dirty), y):
if dirty:
yield move_to(x, offset_y).segment
yield from line
self.dirty[:] = [False] * len(self.lines)

View File

@@ -90,6 +90,9 @@ class App(MessagePump):
Screen(Control.home(), self.view, Control.home(), application_mode=True)
)
async def on_idle(self, event: events.Idle) -> None:
await self.view.post_message(event)
async def action(self, action: str) -> None:
if "." in action:
destination, action_name, *tokens = action.split(".")

View File

@@ -220,7 +220,6 @@ class Enter(Event, type=EventType.ENTER):
yield "y", self.y
@rich_repr
class Leave(Event, type=EventType.LEAVE):
pass

View File

@@ -121,9 +121,11 @@ class MessagePump:
except Exception:
log.exception("error getting message")
break
await self.dispatch_message(message, priority)
if self._message_queue.empty():
await self.dispatch_message(events.Idle(self))
try:
await self.dispatch_message(message, priority)
finally:
if self._message_queue.empty():
await self.dispatch_message(events.Idle(self))
async def dispatch_message(
self, message: Message, priority: int = 0

View File

@@ -81,6 +81,7 @@ class LayoutView(View):
self.focused: Optional[MessagePump] = None
self._widgets: Set[Widget] = set()
super().__init__()
self.enable_messages(events.Idle)
def __rich_repr__(self) -> RichReprResult:
yield "name", self.name
@@ -136,9 +137,6 @@ class LayoutView(View):
)
self.app.refresh()
async def on_idle(self, event: events.Idle) -> None:
pass
async def on_move(self, event: events.Move) -> None:
try:
widget, region = self.get_widget_at(event.x, event.y)

View File

@@ -2,6 +2,7 @@ from logging import getLogger
from typing import (
ClassVar,
Generic,
Iterable,
List,
NamedTuple,
Optional,
@@ -20,6 +21,7 @@ from rich.segment import Segment
from . import events
from ._context import active_app
from ._line_cache import LineCache
from .message import Message
from .message_pump import MessagePump
@@ -44,6 +46,7 @@ class Reactive(Generic[T]):
def __set__(self, obj: "Widget", value: T) -> None:
if getattr(obj, self.internal_name) != value:
log.debug("%s -> %s", self.internal_name, value)
setattr(obj, self.internal_name, value)
obj.require_refresh()
@@ -66,8 +69,7 @@ class Widget(MessagePump):
self.size = WidgetDimensions(0, 0)
self.size_changed = False
self._refresh_required = False
self._dirty_lines: List[bool] = []
self._line_cache: List[List[Segment]] = []
self._line_cache: Optional[LineCache] = None
super().__init__()
if not self.mouse_events:
self.disable_messages(
@@ -94,6 +96,9 @@ class Widget(MessagePump):
has_focus: Reactive[bool] = Reactive(False)
mouse_over: Reactive[bool] = Reactive(False)
def __rich_repr__(self) -> RichReprResult:
yield "name", self.name
@property
def app(self) -> "App":
"""Get the current app."""
@@ -104,25 +109,26 @@ class Widget(MessagePump):
"""Get the current console."""
return active_app.get().console
@property
def line_cache(self) -> LineCache:
if self._line_cache is None:
width, height = self.size
renderable = self.render()
self._line_cache = LineCache.from_renderable(
self.console, renderable, width, height
)
assert self._line_cache is not None
return self._line_cache
def __rich__(self) -> LineCache:
return self.line_cache
def require_refresh(self) -> None:
self._dirty_lines[:] = [True] * len(self._line_cache)
self.app.refresh()
self._line_cache = None
async def refresh(self) -> None:
self.app.refresh()
def __rich_repr__(self) -> RichReprResult:
yield "name", self.name
def render_line_cache(self) -> None:
console = self.console
options = console.options.update_dimensions(self.size.width, self.size.height)
renderable = self.render()
self._line_cache[:] = console.render_lines(renderable, options, new_lines=False)
self._dirty_lines = [True] * len(self._line_cache)
def _clean_line_cache(self) -> None:
self._dirty_lines = [False] * len(self._line_cache)
def render_update(self, x: int, y: int) -> Iterable[Segment]:
yield from self.line_cache.render(x, y)
def render(self) -> RenderableType:
return Panel(
@@ -132,17 +138,6 @@ class Widget(MessagePump):
box=box.HEAVY if self.has_focus else box.ROUNDED,
)
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
self.render_line_cache()
new_line = Segment.line()
for line in self._line_cache:
yield from line
yield new_line
self._clean_line_cache()
self._refresh_required = True
async def post_message(
self, message: Message, priority: Optional[int] = None
) -> bool:
@@ -167,3 +162,6 @@ class Widget(MessagePump):
async def on_blur(self, event: events.Focus) -> None:
self.has_focus = False
async def on_idle(self, event: events.Idle) -> None:
self.app.refresh()