diff --git a/examples/basic.py b/examples/basic.py index 542044e24..69970951e 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -7,7 +7,7 @@ class BasicApp(App): css = """ - App > View { + App > DockView { layout: dock; docks: sidebar=left | widgets=top; } diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index a7baae8c9..f5ae8ffeb 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -6,7 +6,6 @@ from weakref import ref import rich.repr if TYPE_CHECKING: - from .widget import Widget from .dom import DOMNode @@ -20,8 +19,8 @@ class NodeList: """ def __init__(self) -> None: - self._widget_refs: list[ref[DOMNode]] = [] - self.__widgets: list[DOMNode] | None = [] + self._node_refs: list[ref[DOMNode]] = [] + self.__nodes: list[DOMNode] | None = [] def __rich_repr__(self) -> rich.repr.Result: yield self._widgets @@ -29,38 +28,38 @@ class NodeList: def __len__(self) -> int: return len(self._widgets) - def __contains__(self, widget: Widget) -> bool: + def __contains__(self, widget: DOMNode) -> bool: return widget in self._widgets @property def _widgets(self) -> list[DOMNode]: - if self.__widgets is None: - self.__widgets = list( - filter(None, [widget_ref() for widget_ref in self._widget_refs]) + if self.__nodes is None: + self.__nodes = list( + filter(None, [widget_ref() for widget_ref in self._node_refs]) ) - return self.__widgets + return self.__nodes def _prune(self) -> None: """Remove expired references.""" - self._widget_refs[:] = filter( + self._node_refs[:] = filter( None, [ None if widget_ref() is None else widget_ref - for widget_ref in self._widget_refs + for widget_ref in self._node_refs ], ) def _append(self, widget: DOMNode) -> None: if widget not in self._widgets: - self._widget_refs.append(ref(widget)) - self.__widgets = None + self._node_refs.append(ref(widget)) + self.__nodes = None def _clear(self) -> None: - del self._widget_refs[:] - self.__widgets = None + del self._node_refs[:] + self.__nodes = None def __iter__(self) -> Iterator[DOMNode]: - for widget_ref in self._widget_refs: + for widget_ref in self._node_refs: widget = widget_ref() if widget is not None: yield widget diff --git a/src/textual/app.py b/src/textual/app.py index 27f49e969..4304f42e9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3,7 +3,7 @@ import os import asyncio -from typing import Any, Callable, ClassVar, Type, TypeVar +from typing import Any, Callable, ClassVar, Iterable, Type, TypeVar import warnings from rich.control import Control @@ -301,6 +301,7 @@ class App(DOMNode): self.stylesheet.read(self.css_file) if self.css is not None: self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>") + print(self.stylesheet.css) except StylesheetParseError as error: self.panic(error) self._print_error_renderables() @@ -313,8 +314,10 @@ class App(DOMNode): try: load_event = events.Load(sender=self) await self.dispatch_message(load_event) + view = DockView() + await self.mount(view) + await self.push_view(view) await self.post_message(events.Mount(self)) - await self.push_view(DockView()) # Wait for the load event to be processed, so we don't go in to application mode beforehand await load_event.wait() @@ -342,15 +345,27 @@ class App(DOMNode): if self.log_file is not None: self.log_file.close() - def register(self, child: MessagePump, parent: MessagePump) -> bool: + def register(self, child: DOMNode, parent: DOMNode) -> bool: if child not in self.registry: self.registry.add(child) child.set_parent(parent) child.start_messages() child.post_message_no_wait(events.Mount(sender=parent)) + parent.children._append(child) return True return False + async def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: + + name_widgets: Iterable[tuple[str | None, Widget]] + name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()] + apply_stylesheet = self.stylesheet.apply + for widget_id, widget in name_widgets: + if widget_id is not None: + widget.id = widget_id + self.register(widget, self) + apply_stylesheet(widget) + def is_mounted(self, widget: Widget) -> bool: return widget in self.registry diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 3f98d62d9..c116ba881 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -10,19 +10,19 @@ from .match import match from .parse import parse_selectors if TYPE_CHECKING: - from ..dom import DOMNode + from ..widget import Widget @rich.repr.auto(angular=True) class DOMQuery: def __init__( self, - node: DOMNode | None = None, + node: Widget | None = None, selector: str | None = None, - nodes: Iterable[DOMNode] | None = None, + nodes: Iterable[Widget] | None = None, ) -> None: - self._nodes: list[DOMNode] + self._nodes: list[Widget] = [] if nodes is not None: self._nodes = list(nodes) elif node is not None: @@ -34,7 +34,7 @@ class DOMQuery: selector_set = parse_selectors(selector) self._nodes = [_node for _node in self._nodes if match(selector_set, _node)] - def __iter__(self) -> Iterator[DOMNode]: + def __iter__(self) -> Iterator[Widget]: return iter(self._nodes) def __rich_repr__(self) -> rich.repr.Result: @@ -45,3 +45,6 @@ class DOMQuery: query = DOMQuery() query._nodes = [_node for _node in self._nodes if match(selector_set, _node)] return query + + def first(self) -> Widget: + return self._nodes[0] diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 5e00bb39e..36c8c12d2 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -21,6 +21,7 @@ from .model import RuleSet from .parse import parse from .types import Specificity3, Specificity4 from ..dom import DOMNode +from .. import log class StylesheetParseError(Exception): @@ -133,6 +134,7 @@ class Stylesheet: for name, specificity_rules in rule_attributes.items() ] node.styles.apply_rules(node_rules) + log(node, node_rules) if __name__ == "__main__": diff --git a/src/textual/dom.py b/src/textual/dom.py index 04f9fd865..adaf2459e 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable, Iterator, TYPE_CHECKING +from typing import cast, Iterable, Iterator, TYPE_CHECKING from rich.highlighter import ReprHighlighter import rich.repr @@ -14,6 +14,7 @@ from ._node_list import NodeList if TYPE_CHECKING: from .css.query import DOMQuery + from .widget import Widget @rich.repr.auto diff --git a/src/textual/layout.py b/src/textual/layout.py index 7af1e40f6..ce048d011 100644 --- a/src/textual/layout.py +++ b/src/textual/layout.py @@ -15,7 +15,6 @@ from rich.segment import Segment, SegmentLines from rich.style import Style from . import log, panic -from .dom import DOMNode from ._loop import loop_last from .layout_map import LayoutMap from ._profile import timer @@ -150,7 +149,7 @@ class Layout(ABC): ) @abstractmethod - def get_widgets(self, view: View) -> Iterable[DOMNode]: + def get_widgets(self, view: View) -> Iterable[Widget]: ... @abstractmethod diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index 081beb0dd..8dc791609 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -5,11 +5,14 @@ from collections import defaultdict from dataclasses import dataclass from typing import Iterable, TYPE_CHECKING, Sequence +from ..app import active_app + from ..dom import DOMNode from .._layout_resolve import layout_resolve from ..geometry import Offset, Region, Size from ..layout import Layout, WidgetPlacement from ..layout_map import LayoutMap +from ..widget import Widget if sys.version_info >= (3, 8): from typing import Literal @@ -35,7 +38,7 @@ class DockOptions: @dataclass class Dock: edge: str - widgets: Sequence[DOMNode] + widgets: Sequence[Widget] z: int = 0 @@ -45,8 +48,9 @@ class DockLayout(Layout): self._docks: list[Dock] | None = None def get_docks(self, view: View) -> list[Dock]: - groups: dict[str, list[DOMNode]] = defaultdict(list) + groups: dict[str, list[Widget]] = defaultdict(list) for child in view.children: + assert isinstance(child, Widget) groups[child.styles.dock_group].append(child) docks: list[Dock] = [] append_dock = docks.append diff --git a/src/textual/view.py b/src/textual/view.py index e506021fe..b618d252e 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -138,15 +138,7 @@ class View(Widget): self.app.refresh() async def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: - - name_widgets: Iterable[tuple[str | None, Widget]] - name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()] - apply_stylesheet = self.app.stylesheet.apply - for widget_id, widget in name_widgets: - if widget_id is not None: - widget.id = widget_id - apply_stylesheet(widget) - self._add_child(widget) + await self.app.mount(*anon_widgets, **widgets) self.refresh() async def refresh_layout(self) -> None: diff --git a/src/textual/widget.py b/src/textual/widget.py index 2cf144068..9a6ac0f1e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -8,7 +8,6 @@ from typing import ( Callable, ClassVar, NamedTuple, - NewType, cast, ) import rich.repr @@ -116,25 +115,15 @@ class Widget(DOMNode): renderable = self.render_styled() return renderable - def _add_child(self, widget: Widget) -> Widget: - """Add a child widget. - - Args: - widget (Widget): Widget - """ - self.app.register(widget, self) - self.children._append(widget) - return widget - def get_child(self, name: str | None = None, id: str | None = None) -> Widget: if name is not None: for widget in self.children: if widget.name == name: - return widget + return cast(Widget, widget) if id is not None: for widget in self.children: if widget.id == id: - return widget + return cast(Widget, widget) raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}") def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: