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]
|
[tool.poetry]
|
||||||
name = "textual"
|
name = "textual"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Rich TUI"
|
description = "Text User Interface using Rich"
|
||||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"path": "."
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../rich"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"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)
|
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:
|
async def action(self, action: str) -> None:
|
||||||
if "." in action:
|
if "." in action:
|
||||||
destination, action_name, *tokens = action.split(".")
|
destination, action_name, *tokens = action.split(".")
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ class Enter(Event, type=EventType.ENTER):
|
|||||||
yield "y", self.y
|
yield "y", self.y
|
||||||
|
|
||||||
|
|
||||||
@rich_repr
|
|
||||||
class Leave(Event, type=EventType.LEAVE):
|
class Leave(Event, type=EventType.LEAVE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,11 @@ class MessagePump:
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.exception("error getting message")
|
log.exception("error getting message")
|
||||||
break
|
break
|
||||||
await self.dispatch_message(message, priority)
|
try:
|
||||||
if self._message_queue.empty():
|
await self.dispatch_message(message, priority)
|
||||||
await self.dispatch_message(events.Idle(self))
|
finally:
|
||||||
|
if self._message_queue.empty():
|
||||||
|
await self.dispatch_message(events.Idle(self))
|
||||||
|
|
||||||
async def dispatch_message(
|
async def dispatch_message(
|
||||||
self, message: Message, priority: int = 0
|
self, message: Message, priority: int = 0
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class LayoutView(View):
|
|||||||
self.focused: Optional[MessagePump] = None
|
self.focused: Optional[MessagePump] = None
|
||||||
self._widgets: Set[Widget] = set()
|
self._widgets: Set[Widget] = set()
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.enable_messages(events.Idle)
|
||||||
|
|
||||||
def __rich_repr__(self) -> RichReprResult:
|
def __rich_repr__(self) -> RichReprResult:
|
||||||
yield "name", self.name
|
yield "name", self.name
|
||||||
@@ -136,9 +137,6 @@ class LayoutView(View):
|
|||||||
)
|
)
|
||||||
self.app.refresh()
|
self.app.refresh()
|
||||||
|
|
||||||
async def on_idle(self, event: events.Idle) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_move(self, event: events.Move) -> None:
|
async def on_move(self, event: events.Move) -> None:
|
||||||
try:
|
try:
|
||||||
widget, region = self.get_widget_at(event.x, event.y)
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from logging import getLogger
|
|||||||
from typing import (
|
from typing import (
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Generic,
|
Generic,
|
||||||
|
Iterable,
|
||||||
List,
|
List,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -20,6 +21,7 @@ from rich.segment import Segment
|
|||||||
|
|
||||||
from . import events
|
from . import events
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
|
from ._line_cache import LineCache
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ class Reactive(Generic[T]):
|
|||||||
|
|
||||||
def __set__(self, obj: "Widget", value: T) -> None:
|
def __set__(self, obj: "Widget", value: T) -> None:
|
||||||
if getattr(obj, self.internal_name) != value:
|
if getattr(obj, self.internal_name) != value:
|
||||||
|
log.debug("%s -> %s", self.internal_name, value)
|
||||||
setattr(obj, self.internal_name, value)
|
setattr(obj, self.internal_name, value)
|
||||||
obj.require_refresh()
|
obj.require_refresh()
|
||||||
|
|
||||||
@@ -66,8 +69,7 @@ class Widget(MessagePump):
|
|||||||
self.size = WidgetDimensions(0, 0)
|
self.size = WidgetDimensions(0, 0)
|
||||||
self.size_changed = False
|
self.size_changed = False
|
||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
self._dirty_lines: List[bool] = []
|
self._line_cache: Optional[LineCache] = None
|
||||||
self._line_cache: List[List[Segment]] = []
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not self.mouse_events:
|
if not self.mouse_events:
|
||||||
self.disable_messages(
|
self.disable_messages(
|
||||||
@@ -94,6 +96,9 @@ class Widget(MessagePump):
|
|||||||
has_focus: Reactive[bool] = Reactive(False)
|
has_focus: Reactive[bool] = Reactive(False)
|
||||||
mouse_over: Reactive[bool] = Reactive(False)
|
mouse_over: Reactive[bool] = Reactive(False)
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> RichReprResult:
|
||||||
|
yield "name", self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self) -> "App":
|
def app(self) -> "App":
|
||||||
"""Get the current app."""
|
"""Get the current app."""
|
||||||
@@ -104,25 +109,26 @@ class Widget(MessagePump):
|
|||||||
"""Get the current console."""
|
"""Get the current console."""
|
||||||
return active_app.get().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:
|
def require_refresh(self) -> None:
|
||||||
self._dirty_lines[:] = [True] * len(self._line_cache)
|
self._line_cache = None
|
||||||
self.app.refresh()
|
|
||||||
|
|
||||||
async def refresh(self) -> None:
|
def render_update(self, x: int, y: int) -> Iterable[Segment]:
|
||||||
self.app.refresh()
|
yield from self.line_cache.render(x, y)
|
||||||
|
|
||||||
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(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return Panel(
|
return Panel(
|
||||||
@@ -132,17 +138,6 @@ class Widget(MessagePump):
|
|||||||
box=box.HEAVY if self.has_focus else box.ROUNDED,
|
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(
|
async def post_message(
|
||||||
self, message: Message, priority: Optional[int] = None
|
self, message: Message, priority: Optional[int] = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -167,3 +162,6 @@ class Widget(MessagePump):
|
|||||||
|
|
||||||
async def on_blur(self, event: events.Focus) -> None:
|
async def on_blur(self, event: events.Focus) -> None:
|
||||||
self.has_focus = False
|
self.has_focus = False
|
||||||
|
|
||||||
|
async def on_idle(self, event: events.Idle) -> None:
|
||||||
|
self.app.refresh()
|
||||||
|
|||||||
Reference in New Issue
Block a user