This commit is contained in:
Will McGugan
2021-10-28 11:44:14 +01:00
parent 3e74b5e0b0
commit 6ca8da3e14
9 changed files with 143 additions and 85 deletions

View File

@@ -7,10 +7,11 @@ import rich.repr
if TYPE_CHECKING: if TYPE_CHECKING:
from .widget import Widget from .widget import Widget
from .dom import DOMNode
@rich.repr.auto @rich.repr.auto
class WidgetList: class NodeList:
""" """
A container for widgets that forms one level of hierarchy. A container for widgets that forms one level of hierarchy.
@@ -19,8 +20,8 @@ class WidgetList:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._widget_refs: list[ref[Widget]] = [] self._widget_refs: list[ref[DOMNode]] = []
self.__widgets: list[Widget] | None = [] self.__widgets: list[DOMNode] | None = []
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield self._widgets yield self._widgets
@@ -32,7 +33,7 @@ class WidgetList:
return widget in self._widgets return widget in self._widgets
@property @property
def _widgets(self) -> list[Widget]: def _widgets(self) -> list[DOMNode]:
if self.__widgets is None: if self.__widgets is None:
self.__widgets = list( self.__widgets = list(
filter(None, [widget_ref() for widget_ref in self._widget_refs]) filter(None, [widget_ref() for widget_ref in self._widget_refs])
@@ -49,7 +50,7 @@ class WidgetList:
], ],
) )
def _append(self, widget: Widget) -> None: def _append(self, widget: DOMNode) -> None:
if widget not in self._widgets: if widget not in self._widgets:
self._widget_refs.append(ref(widget)) self._widget_refs.append(ref(widget))
self.__widgets = None self.__widgets = None
@@ -58,21 +59,21 @@ class WidgetList:
del self._widget_refs[:] del self._widget_refs[:]
self.__widgets = None self.__widgets = None
def __iter__(self) -> Iterable[Widget]: def __iter__(self) -> Iterable[DOMNode]:
for widget_ref in self._widget_refs: for widget_ref in self._widget_refs:
widget = widget_ref() widget = widget_ref()
if widget is not None: if widget is not None:
yield widget yield widget
@overload @overload
def __getitem__(self, index: int) -> Widget: def __getitem__(self, index: int) -> DOMNode:
... ...
@overload @overload
def __getitem__(self, index: slice) -> list[Widget]: def __getitem__(self, index: slice) -> list[DOMNode]:
... ...
def __getitem__(self, index: int | slice) -> Widget | list[Widget]: def __getitem__(self, index: int | slice) -> DOMNode | list[DOMNode]:
self._prune() self._prune()
assert self._widgets is not None assert self._widgets is not None
return self._widgets[index] return self._widgets[index]

View File

@@ -18,6 +18,7 @@ from rich.tree import Tree
from . import events from . import events
from . import actions from . import actions
from .dom import DOMNode
from ._animator import Animator from ._animator import Animator
from .binding import Bindings, NoBinding from .binding import Bindings, NoBinding
from .geometry import Offset, Region from .geometry import Offset, Region
@@ -57,7 +58,7 @@ class ActionError(Exception):
@rich.repr.auto @rich.repr.auto
class App(MessagePump): class App(DOMNode):
"""The base class for Textual Applications""" """The base class for Textual Applications"""
KEYS: ClassVar[dict[str, str]] = {} KEYS: ClassVar[dict[str, str]] = {}
@@ -88,7 +89,6 @@ class App(MessagePump):
self._title = title self._title = title
self._layout = DockLayout() self._layout = DockLayout()
self._view_stack: list[DockView] = [] self._view_stack: list[DockView] = []
self.children: set[Widget] = set()
self.focused: Widget | None = None self.focused: Widget | None = None
self.mouse_over: Widget | None = None self.mouse_over: Widget | None = None
@@ -115,6 +115,8 @@ class App(MessagePump):
self.css_file = css_file self.css_file = css_file
self.css = css self.css = css
self.registry: set[MessagePump] = set()
super().__init__() super().__init__()
title: Reactive[str] = Reactive("Textual") title: Reactive[str] = Reactive("Textual")
@@ -150,6 +152,10 @@ class App(MessagePump):
add_children(branch, self.view) add_children(branch, self.view)
return tree return tree
@property
def css_type(self) -> str:
return "app"
def load_css(self, filename: str) -> None: def load_css(self, filename: str) -> None:
pass pass
@@ -340,8 +346,8 @@ class App(MessagePump):
self.log_file.close() self.log_file.close()
def register(self, child: MessagePump, parent: MessagePump) -> bool: def register(self, child: MessagePump, parent: MessagePump) -> bool:
if child not in self.children: if child not in self.registry:
self.children.add(child) self.registry.add(child)
child.set_parent(parent) child.set_parent(parent)
child.start_messages() child.start_messages()
child.post_message_no_wait(events.Mount(sender=parent)) child.post_message_no_wait(events.Mount(sender=parent))
@@ -349,15 +355,15 @@ class App(MessagePump):
return False return False
def is_mounted(self, widget: Widget) -> bool: def is_mounted(self, widget: Widget) -> bool:
return widget in self.children return widget in self.registry
async def close_all(self) -> None: async def close_all(self) -> None:
while self.children: while self.registry:
child = self.children.pop() child = self.registry.pop()
await child.close_messages() await child.close_messages()
async def remove(self, child: MessagePump) -> None: async def remove(self, child: MessagePump) -> None:
self.children.remove(child) self.registry.remove(child)
async def shutdown(self): async def shutdown(self):
driver = self._driver driver = self._driver

View File

@@ -8,6 +8,7 @@ from typing import Iterable
from .styles import Styles from .styles import Styles
from .tokenize import Token from .tokenize import Token
from .types import Specificity3
class SelectorType(Enum): class SelectorType(Enum):
@@ -33,18 +34,18 @@ class Location:
class Selector: class Selector:
name: str name: str
combinator: CombinatorType = CombinatorType.SAME combinator: CombinatorType = CombinatorType.SAME
selector: SelectorType = SelectorType.TYPE type: SelectorType = SelectorType.TYPE
pseudo_classes: list[str] = field(default_factory=list) pseudo_classes: list[str] = field(default_factory=list)
specificity: tuple[int, int, int] = field(default_factory=lambda: (0, 0, 0)) specificity: Specificity3 = field(default_factory=lambda: (0, 0, 0))
@property @property
def css(self) -> str: def css(self) -> str:
psuedo_suffix = "".join(f":{name}" for name in self.pseudo_classes) psuedo_suffix = "".join(f":{name}" for name in self.pseudo_classes)
if self.selector == SelectorType.UNIVERSAL: if self.type == SelectorType.UNIVERSAL:
return "*" return "*"
elif self.selector == SelectorType.TYPE: elif self.type == SelectorType.TYPE:
return f"{self.name}{psuedo_suffix}" return f"{self.name}{psuedo_suffix}"
elif self.selector == SelectorType.CLASS: elif self.type == SelectorType.CLASS:
return f".{self.name}{psuedo_suffix}" return f".{self.name}{psuedo_suffix}"
else: else:
return f"#{self.name}{psuedo_suffix}" return f"#{self.name}{psuedo_suffix}"
@@ -55,14 +56,11 @@ class Declaration:
name: str name: str
tokens: list[Token] = field(default_factory=list) tokens: list[Token] = field(default_factory=list)
def process(self):
raise NotImplementedError
@dataclass @dataclass
class SelectorSet: class SelectorSet:
selectors: list[Selector] = field(default_factory=list) selectors: list[Selector] = field(default_factory=list)
specificity: tuple[int, int, int] = (0, 0, 0) specificity: Specificity3 = (0, 0, 0)
@classmethod @classmethod
def from_selectors(cls, selectors: list[list[Selector]]) -> Iterable[SelectorSet]: def from_selectors(cls, selectors: list[list[Selector]]) -> Iterable[SelectorSet]:

View File

@@ -59,7 +59,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
Selector( Selector(
name=token.value.lstrip(".#"), name=token.value.lstrip(".#"),
combinator=combinator, combinator=combinator,
selector=_selector, type=_selector,
specificity=specificity, specificity=specificity,
) )
) )

View File

@@ -113,12 +113,12 @@ class Styles:
def extract_rules( def extract_rules(
self, specificity: tuple[int, int, int] self, specificity: tuple[int, int, int]
) -> dict[str, tuple[object, tuple[int, int, int, int]]]: ) -> dict[str, tuple[tuple[int, int, int, int], object]]:
is_important = self.important.__contains__ is_important = self.important.__contains__
return { return {
rule_name: ( rule_name: (
getattr(self, rule_name),
(int(is_important(rule_name)), *specificity), (int(is_important(rule_name)), *specificity),
getattr(self, rule_name),
) )
for rule_name in RULE_NAMES for rule_name in RULE_NAMES
if getattr(self, f"_rule_{rule_name}") is not None if getattr(self, f"_rule_{rule_name}") is not None

View File

@@ -2,9 +2,14 @@ from __future__ import annotations
import rich.repr import rich.repr
from ..dom import DOMNode
from .errors import StylesheetError from .errors import StylesheetError
from .model import RuleSet from .model import CombinatorType, RuleSet, Selector, SelectorType
from .parse import parse from .parse import parse
from .styles import Styles
from .types import Specificity3
from ..widget import Widget
@rich.repr.auto @rich.repr.auto
@@ -37,3 +42,34 @@ class Stylesheet:
except Exception as error: except Exception as error:
raise StylesheetError(f"failed to parse css; {error}") from None raise StylesheetError(f"failed to parse css; {error}") from None
self.rules.extend(rules) self.rules.extend(rules)
def apply(self, node: DOMNode) -> None:
styles: list[tuple[Specificity3, Styles]] = []
for rule in self.rules:
self.apply_rule(rule, node)
def apply_rule(self, rule: RuleSet, node: DOMNode) -> None:
for selector_set in rule.selector_set:
self.check_selectors(selector_set.selectors, node)
def check_selectors(self, selectors: list[Selector], node: DOMNode) -> bool:
node_path = node.css_path
nodes = iter(node_path)
node, siblings = next(nodes)
for selector in selectors:
if selector.type == SelectorType.UNIVERSAL:
continue
elif selector.type == SelectorType.TYPE:
while node.css_type != selector.name:
node, siblings = next(nodes)
if node is None:
return False
elif selector.type == SelectorType.CLASS:
while node.css_type != selector.name:
node, siblings = next(nodes)
if node is None:
return False
return True

View File

@@ -15,3 +15,5 @@ else:
Visibility = Literal["visible", "hidden", "initial", "inherit"] Visibility = Literal["visible", "hidden", "initial", "inherit"]
Display = Literal["block", "none"] Display = Literal["block", "none"]
EdgeStyle = Tuple[str, Style] EdgeStyle = Tuple[str, Style]
Specificity3 = Tuple[int, int, int]
Specificity4 = Tuple[int, int, int, int]

61
src/textual/dom.py Normal file
View File

@@ -0,0 +1,61 @@
from __future__ import annotations
import rich.repr
from .css.styles import Styles
from .message_pump import MessagePump
from ._node_list import NodeList
@rich.repr.auto
class DOMNode(MessagePump):
def __init__(self, name: str | None = None, id: str | None = None) -> None:
self._name = name
self._id = id
self._class_names: set[str] = set()
self.children = NodeList()
self.styles: Styles = Styles()
super().__init__()
@property
def id(self) -> str | None:
return self._id
@property
def name(self) -> str | None:
return self._name
@property
def class_names(self) -> frozenset[str]:
return frozenset(self._class_names)
@property
def css_type(self) -> str:
return self.__class__.__name__.lower()
@property
def css_path(self) -> list[tuple[DOMNode, list[DOMNode]]]:
result: list[tuple[DOMNode, list[DOMNode]]] = []
append = result.append
# TODO:
node: DOMNode = self
while isinstance(node._parent, DOMNode):
append((node, node.children[:]))
node = node._parent
return result[::-1]
def has_class(self, *class_names: str) -> bool:
return self._class_names.issuperset(class_names)
def add_class(self, *class_names: str) -> None:
"""Add class names."""
self._class_names.update(class_names)
def remove_class(self, *class_names: str) -> None:
"""Remove class names"""
self._class_names.difference_update(class_names)
def toggle_class(self, *class_names: str) -> None:
"""Toggle class names"""
self._class_names.symmetric_difference_update(class_names)

View File

@@ -27,14 +27,12 @@ from . import errors
from ._animator import BoundAnimator from ._animator import BoundAnimator
from ._border import Border, BORDER_STYLES from ._border import Border, BORDER_STYLES
from ._callback import invoke from ._callback import invoke
from ._widget_list import WidgetList from .dom import DOMNode
from ._context import active_app from ._context import active_app
from .geometry import Size, Spacing, SpacingDimensions from .geometry import Size, Spacing, SpacingDimensions
from .message import Message from .message import Message
from .message_pump import MessagePump
from .messages import Layout, Update from .messages import Layout, Update
from .reactive import Reactive, watch from .reactive import Reactive, watch
from .css.styles import Styles
from ._types import Lines from ._types import Lines
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -58,18 +56,17 @@ class RenderCache(NamedTuple):
@rich.repr.auto @rich.repr.auto
class Widget(MessagePump): class Widget(DOMNode):
_counts: ClassVar[dict[str, int]] = {} _counts: ClassVar[dict[str, int]] = {}
can_focus: bool = False can_focus: bool = False
def __init__(self, name: str | None = None, id: str | None = None) -> None: def __init__(self, name: str | None = None, id: str | None = None) -> None:
class_name = self.__class__.__name__ if name is None:
Widget._counts.setdefault(class_name, 0) class_name = self.__class__.__name__
Widget._counts[class_name] += 1 Widget._counts.setdefault(class_name, 0)
_count = self._counts[class_name] Widget._counts[class_name] += 1
_count = self._counts[class_name]
self.name = name or f"_{class_name}{_count}" name = f"{class_name}{_count}"
self._id = id
self._size = Size(0, 0) self._size = Size(0, 0)
self._repaint_required = False self._repaint_required = False
@@ -78,12 +75,8 @@ class Widget(MessagePump):
self._reactive_watches: dict[str, Callable] = {} self._reactive_watches: dict[str, Callable] = {}
self.render_cache: RenderCache | None = None self.render_cache: RenderCache | None = None
self.highlight_style: Style | None = None self.highlight_style: Style | None = None
self._class_names: set[str] = set()
self.styles: Styles = Styles() super().__init__(name=name, id=id)
self.children = WidgetList()
super().__init__()
visible: Reactive[bool] = Reactive(True, layout=True) visible: Reactive[bool] = Reactive(True, layout=True)
layout_size: Reactive[int | None] = Reactive(None, layout=True) layout_size: Reactive[int | None] = Reactive(None, layout=True)
@@ -144,45 +137,6 @@ class Widget(MessagePump):
return widget return widget
raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}") raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}")
@property
def id(self) -> str | None:
return self._id
@property
def class_names(self) -> frozenset[str]:
return frozenset(self._class_names)
@property
def _css_path(self) -> list[tuple[MessagePump, list[Widget]]]:
result: list[tuple[MessagePump, list[Widget]]] = []
# TODO:
widget: Widget = self
while isinstance(widget._parent, Widget):
result.append((widget, widget.children[:]))
widget = widget._parent
return result[::-1]
def has_class(self, *class_names: str) -> bool:
return self._class_names.issuperset(class_names)
def add_class(self, *class_names: str) -> None:
"""Add class names."""
self._class_names.update(class_names)
def remove_class(self, *class_names: str) -> None:
"""Remove class names"""
self._class_names.difference_update(class_names)
def toggle_class(self, *class_names: str) -> None:
"""Toggle class names"""
_class_names = self._class_names
for class_name in class_names:
if class_name in _class_names:
_class_names.discard(class_name)
else:
_class_names.add(class_name)
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
watch(self, attribute_name, callback) watch(self, attribute_name, callback)