mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
layout property, and app.tree
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
31
src/textual/layouts/factory.py
Normal file
31
src/textual/layouts/factory.py
Normal 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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user