mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
line cache
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../rich"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
39
src/textual/_line_cache.py
Normal file
39
src/textual/_line_cache.py
Normal 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)
|
||||
@@ -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(".")
|
||||
|
||||
@@ -220,7 +220,6 @@ class Enter(Event, type=EventType.ENTER):
|
||||
yield "y", self.y
|
||||
|
||||
|
||||
@rich_repr
|
||||
class Leave(Event, type=EventType.LEAVE):
|
||||
pass
|
||||
|
||||
|
||||
@@ -121,7 +121,9 @@ class MessagePump:
|
||||
except Exception:
|
||||
log.exception("error getting message")
|
||||
break
|
||||
try:
|
||||
await self.dispatch_message(message, priority)
|
||||
finally:
|
||||
if self._message_queue.empty():
|
||||
await self.dispatch_message(events.Idle(self))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
def require_refresh(self) -> None:
|
||||
self._dirty_lines[:] = [True] * len(self._line_cache)
|
||||
self.app.refresh()
|
||||
@property
|
||||
def line_cache(self) -> LineCache:
|
||||
|
||||
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)
|
||||
if self._line_cache is None:
|
||||
width, height = self.size
|
||||
renderable = self.render()
|
||||
self._line_cache[:] = console.render_lines(renderable, options, new_lines=False)
|
||||
self._dirty_lines = [True] * len(self._line_cache)
|
||||
self._line_cache = LineCache.from_renderable(
|
||||
self.console, renderable, width, height
|
||||
)
|
||||
assert self._line_cache is not None
|
||||
return self._line_cache
|
||||
|
||||
def _clean_line_cache(self) -> None:
|
||||
self._dirty_lines = [False] * len(self._line_cache)
|
||||
def __rich__(self) -> LineCache:
|
||||
return self.line_cache
|
||||
|
||||
def require_refresh(self) -> None:
|
||||
self._line_cache = None
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user