layout property, and app.tree

This commit is contained in:
Will McGugan
2021-10-21 17:08:32 +01:00
parent 537abade11
commit 9704945e04
15 changed files with 136 additions and 50 deletions

View File

@@ -27,6 +27,8 @@ class MyApp(App):
# Dock the body in the remaining space
await self.view.dock(body, edge="right")
# self.panic(self.tree)
async def get_markdown(filename: str) -> None:
with open(filename, "rt") as fh:
readme = Markdown(fh.read(), hyperlinks=True)

View File

@@ -49,7 +49,7 @@ class WidgetList:
],
)
def _append(self, widget: Widget):
def _append(self, widget: Widget) -> None:
if widget not in self._widgets:
self._widget_refs.append(ref(widget))
self.__widgets = None

View File

@@ -7,11 +7,14 @@ from typing import Any, Callable, ClassVar, Type, TypeVar
import warnings
from rich.control import Control
from rich.highlighter import ReprHighlighter
import rich.repr
from rich.screen import Screen
from rich.console import Console, RenderableType
from rich.measure import Measurement
from rich.pretty import Pretty
from rich.traceback import Traceback
from rich.tree import Tree
from . import events
from . import actions
@@ -130,6 +133,21 @@ class App(MessagePump):
def view(self) -> DockView:
return self._view_stack[-1]
@property
def tree(self) -> Tree:
highlighter = ReprHighlighter()
tree = Tree(highlighter(repr(self)))
def add_children(tree, node):
for child in node.children:
branch = tree.add(Pretty(child))
if tree.children:
add_children(branch, child)
branch = tree.add(Pretty(self.view))
add_children(branch, self.view)
return tree
def load_css(self, filename: str) -> None:
pass

View File

@@ -6,8 +6,7 @@ from itertools import chain
from operator import itemgetter
import sys
from typing import Iterable, Iterator, NamedTuple, TYPE_CHECKING
from rich import segment
from typing import ClassVar, Iterable, Iterator, NamedTuple, TYPE_CHECKING
import rich.repr
from rich.control import Control
@@ -87,6 +86,8 @@ class LayoutUpdate:
class Layout(ABC):
"""Responsible for arranging Widgets in a view and rendering them."""
name: ClassVar[str] = ""
def __init__(self) -> None:
self._layout_map: LayoutMap | None = None
self.width = 0

View File

@@ -0,0 +1,31 @@
from __future__ import annotations
from ..layout import Layout
from .dock import DockLayout
from .grid import GridLayout
from .vertical import VerticalLayout
class MissingLayout(Exception):
pass
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
def get_layout(name: str) -> Layout:
"""Get a named layout object.
Args:
name (str): Name of the layout.
Raises:
MissingLayout: If the named layout doesn't exist.
Returns:
Layout: A layout object.
"""
layout_class = LAYOUT_MAP.get(name)
if layout_class is None:
raise MissingLayout("no layout called {name!r}")
return layout_class()

View File

@@ -8,12 +8,9 @@ from itertools import cycle, product
import sys
from typing import Iterable, NamedTuple
from rich.console import Console
from .._layout_resolve import layout_resolve
from ..geometry import Size, Offset, Region
from ..layout import Layout, WidgetPlacement
from ..layout_map import LayoutMap
from ..widget import Widget
if sys.version_info >= (3, 8):

View File

@@ -187,6 +187,7 @@ class ScrollBar(Widget):
grabbed: Reactive[Offset | None] = Reactive(None)
def __rich_repr__(self) -> rich.repr.Result:
yield from super().__rich_repr__()
yield "virtual_size", self.virtual_size
yield "window_size", self.window_size
yield "position", self.position

View File

@@ -12,6 +12,7 @@ from . import errors
from . import log
from . import messages
from .layout import Layout, NoWidget, WidgetPlacement
from .layouts.factory import get_layout
from .geometry import Size, Offset, Region
from .reactive import Reactive, watch
@@ -22,13 +23,27 @@ if TYPE_CHECKING:
from .app import App
class LayoutProperty:
def __get__(self, obj: View, objtype: type[View] | None = None) -> str:
return obj._layout.name
def __set__(self, obj: View, layout: str | Layout) -> str:
if isinstance(layout, str):
new_layout = get_layout(layout)
else:
new_layout = layout
self._layout = new_layout
return self._layout.name
@rich.repr.auto
class View(Widget):
layout_factory: ClassVar[Callable[[], Layout]]
def __init__(self, layout: Layout = None, name: str | None = None) -> None:
self.layout: Layout = layout or self.layout_factory()
self._layout: Layout = layout or self.layout_factory()
self.mouse_over: Widget | None = None
self.widgets: set[Widget] = set()
self._mouse_style: Style = Style()
@@ -49,13 +64,27 @@ class View(Widget):
cls.layout_factory = layout
super().__init_subclass__(**kwargs)
# @property
# def layout(self) -> str:
# return self._layout
# @layout.setter
# def layout(self, layout: str | Layout) -> None:
# if isinstance(layout, str):
# new_layout = get_layout(layout)
# else:
# new_layout = layout
# self._layout = new_layout
layout = LayoutProperty()
background: Reactive[str] = Reactive("")
scroll_x: Reactive[int] = Reactive(0)
scroll_y: Reactive[int] = Reactive(0)
virtual_size = Reactive(Size(0, 0))
async def watch_background(self, value: str) -> None:
self.layout.background = value
self._layout.background = value
self.app.refresh()
@property
@@ -89,16 +118,16 @@ class View(Widget):
return self.app.is_mounted(widget)
def render(self) -> RenderableType:
return self.layout
return self._layout
def get_offset(self, widget: Widget) -> Offset:
return self.layout.get_offset(widget)
return self._layout.get_offset(widget)
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
cached_size, cached_scroll, arrangement = self._cached_arrangement
if cached_size == size and cached_scroll == scroll:
return arrangement
arrangement = list(self.layout.arrange(self, size, scroll))
arrangement = list(self._layout.arrange(self, size, scroll))
self._cached_arrangement = (size, scroll, arrangement)
return arrangement
@@ -108,8 +137,7 @@ class View(Widget):
widget = message.widget
assert isinstance(widget, Widget)
display_update = self.layout.update_widget(self.console, widget)
# self.log("UPDATE", widget, display_update)
display_update = self._layout.update_widget(self.console, widget)
if display_update is not None:
self.app.display(display_update)
@@ -135,7 +163,7 @@ class View(Widget):
async def refresh_layout(self) -> None:
self._cached_arrangement = (Size(), Offset(), [])
try:
await self.layout.mount_all(self)
await self._layout.mount_all(self)
if not self.is_root_view:
await self.app.view.refresh_layout()
return
@@ -143,8 +171,8 @@ class View(Widget):
if not self.size:
return
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
assert self.layout.map is not None
hidden, shown, resized = self._layout.reflow(self, Size(*self.console.size))
assert self._layout.map is not None
for widget in hidden:
widget.post_message_no_wait(events.Hide(self))
@@ -154,7 +182,7 @@ class View(Widget):
send_resize = shown
send_resize.update(resized)
for widget, region, unclipped_region in self.layout:
for widget, region, unclipped_region in self._layout:
widget._update_size(unclipped_region.size)
if widget in send_resize:
widget.post_message_no_wait(
@@ -171,13 +199,13 @@ class View(Widget):
event.stop()
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
return self.layout.get_widget_at(x, y)
return self._layout.get_widget_at(x, y)
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)
return self._layout.get_widget_region(widget)
async def on_mount(self, event: events.Mount) -> None:
async def watch_background(value: str) -> None:
@@ -186,8 +214,8 @@ class View(Widget):
watch(self.app, "background", watch_background)
async def on_idle(self, event: events.Idle) -> None:
if self.layout.check_update():
self.layout.reset_update()
if self._layout.check_update():
self._layout.reset_update()
await self.refresh_layout()
async def _on_mouse_move(self, event: events.MouseMove) -> None:

View File

@@ -28,8 +28,8 @@ class DockView(View):
) -> None:
dock = Dock(edge, widgets, z)
assert isinstance(self.layout, DockLayout)
self.layout.docks.append(dock)
assert isinstance(self._layout, DockLayout)
self._layout.docks.append(dock)
for widget in widgets:
if size is not do_not_set:
widget.layout_size = cast(Optional[int], size)
@@ -54,8 +54,8 @@ class DockView(View):
grid = GridLayout(gap=gap, gutter=gutter, align=align)
view = View(layout=grid, name=name)
dock = Dock(edge, (view,), z)
assert isinstance(self.layout, DockLayout)
self.layout.docks.append(dock)
assert isinstance(self._layout, DockLayout)
self._layout.docks.append(dock)
if size is not do_not_set:
view.layout_size = cast(Optional[int], size)
if name is None:

View File

@@ -32,12 +32,12 @@ class WindowView(View, layout=VerticalLayout):
super().__init__(name=name, layout=layout)
async def update(self, widget: Widget | RenderableType) -> None:
layout = self.layout
layout = self._layout
assert isinstance(layout, VerticalLayout)
layout.clear()
self.widget = widget if isinstance(widget, Widget) else Static(widget)
layout.add(self.widget)
self.layout.require_update()
layout.require_update()
self.refresh(layout=True)
await self.emit(WindowChange(self))
@@ -46,8 +46,7 @@ class WindowView(View, layout=VerticalLayout):
await self.emit(WindowChange(self))
async def handle_layout(self, message: messages.Layout) -> None:
self.log("TRANSLATING layout")
self.layout.require_update()
self._layout.require_update()
message.stop()
self.refresh()
@@ -55,11 +54,11 @@ class WindowView(View, layout=VerticalLayout):
await self.emit(WindowChange(self))
async def watch_scroll_x(self, value: int) -> None:
self.layout.require_update()
self._layout.require_update()
self.refresh()
async def watch_scroll_y(self, value: int) -> None:
self.layout.require_update()
self._layout.require_update()
self.refresh()
async def on_resize(self, event: events.Resize) -> None:

View File

@@ -67,7 +67,7 @@ class Widget(MessagePump):
Widget._counts[class_name] += 1
_count = self._counts[class_name]
self.name = name or f"{class_name}#{_count}"
self.name = name or f"_{class_name}{_count}"
self._id = id
self._size = Size(0, 0)
@@ -118,13 +118,14 @@ class Widget(MessagePump):
cls.can_focus = can_focus
def __rich_repr__(self) -> rich.repr.Result:
yield "id", self.id, None
yield "name", self.name
def __rich__(self) -> RenderableType:
renderable = self.render_styled()
return renderable
def add_child(self, widget: Widget) -> None:
def add_child(self, widget: Widget) -> Widget:
"""Add a child widget.
Args:
@@ -132,6 +133,7 @@ class Widget(MessagePump):
"""
self.app.register(widget, self)
self.children._append(widget)
return widget
def get_child(self, name: str | None = None) -> Widget:
for widget in self.children:

View File

@@ -33,7 +33,7 @@ class Footer(Widget):
self.highlight_key = None
def __rich_repr__(self) -> rich.repr.Result:
yield "keys", self.keys
yield from super().__rich_repr__()
def make_key_text(self) -> Text:
"""Create text containing all the keys."""

View File

@@ -39,7 +39,8 @@ class Header(Widget):
return f"{self.title} - {self.sub_title}" if self.sub_title else self.title
def __rich_repr__(self) -> Result:
yield self.title
yield from super().__rich_repr__()
yield "title", self.title
async def watch_tall(self, tall: bool) -> None:
self.layout_size = 3 if tall else 1

View File

@@ -29,7 +29,7 @@ class Placeholder(Widget, can_focus=True):
self.height = height
def __rich_repr__(self) -> rich.repr.Result:
yield "name", self.name
yield from super().__rich_repr__()
yield "has_focus", self.has_focus, False
yield "mouse_over", self.mouse_over, False

View File

@@ -31,13 +31,20 @@ class ScrollView(View):
) -> None:
from ..views import WindowView
self.fluid = fluid
self.vscroll = ScrollBar(vertical=True)
self.hscroll = ScrollBar(vertical=False)
self.window = WindowView(
"" if contents is None else contents, auto_width=auto_width, gutter=gutter
)
layout = GridLayout()
super().__init__(name=name, layout=layout)
self.fluid = fluid
self.vscroll = self.add_child(ScrollBar(vertical=True))
self.hscroll = self.add_child(ScrollBar(vertical=False))
self.window = self.add_child(
WindowView(
"" if contents is None else contents,
auto_width=auto_width,
gutter=gutter,
)
)
layout.add_column("main")
layout.add_column("vscroll", size=1)
layout.add_row("main")
@@ -47,7 +54,6 @@ class ScrollView(View):
)
layout.show_row("hscroll", False)
layout.show_column("vscroll", False)
super().__init__(name=name, layout=layout)
x: Reactive[float] = Reactive(0, repaint=False)
y: Reactive[float] = Reactive(0, repaint=False)
@@ -89,13 +95,13 @@ class ScrollView(View):
await self.window.update(renderable)
async def on_mount(self, event: events.Mount) -> None:
assert isinstance(self.layout, GridLayout)
self.layout.place(
assert isinstance(self._layout, GridLayout)
self._layout.place(
content=self.window,
vscroll=self.vscroll,
hscroll=self.hscroll,
)
await self.layout.mount_all(self)
await self._layout.mount_all(self)
def home(self) -> None:
self.x = self.y = 0
@@ -211,10 +217,10 @@ class ScrollView(View):
self.vscroll.virtual_size = virtual_height
self.vscroll.window_size = height
assert isinstance(self.layout, GridLayout)
assert isinstance(self._layout, GridLayout)
vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
vscroll_change = self._layout.show_column("vscroll", virtual_height > height)
hscroll_change = self._layout.show_row("hscroll", virtual_width > width)
if hscroll_change or vscroll_change:
self.refresh(layout=True)