mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
eof
This commit is contained in:
@@ -7,10 +7,11 @@ import rich.repr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
from .dom import DOMNode
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class WidgetList:
|
||||
class NodeList:
|
||||
"""
|
||||
A container for widgets that forms one level of hierarchy.
|
||||
|
||||
@@ -19,8 +20,8 @@ class WidgetList:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._widget_refs: list[ref[Widget]] = []
|
||||
self.__widgets: list[Widget] | None = []
|
||||
self._widget_refs: list[ref[DOMNode]] = []
|
||||
self.__widgets: list[DOMNode] | None = []
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield self._widgets
|
||||
@@ -32,7 +33,7 @@ class WidgetList:
|
||||
return widget in self._widgets
|
||||
|
||||
@property
|
||||
def _widgets(self) -> list[Widget]:
|
||||
def _widgets(self) -> list[DOMNode]:
|
||||
if self.__widgets is None:
|
||||
self.__widgets = list(
|
||||
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:
|
||||
self._widget_refs.append(ref(widget))
|
||||
self.__widgets = None
|
||||
@@ -58,21 +59,21 @@ class WidgetList:
|
||||
del self._widget_refs[:]
|
||||
self.__widgets = None
|
||||
|
||||
def __iter__(self) -> Iterable[Widget]:
|
||||
def __iter__(self) -> Iterable[DOMNode]:
|
||||
for widget_ref in self._widget_refs:
|
||||
widget = widget_ref()
|
||||
if widget is not None:
|
||||
yield widget
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: int) -> Widget:
|
||||
def __getitem__(self, index: int) -> DOMNode:
|
||||
...
|
||||
|
||||
@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()
|
||||
assert self._widgets is not None
|
||||
return self._widgets[index]
|
||||
@@ -18,6 +18,7 @@ from rich.tree import Tree
|
||||
|
||||
from . import events
|
||||
from . import actions
|
||||
from .dom import DOMNode
|
||||
from ._animator import Animator
|
||||
from .binding import Bindings, NoBinding
|
||||
from .geometry import Offset, Region
|
||||
@@ -57,7 +58,7 @@ class ActionError(Exception):
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class App(MessagePump):
|
||||
class App(DOMNode):
|
||||
"""The base class for Textual Applications"""
|
||||
|
||||
KEYS: ClassVar[dict[str, str]] = {}
|
||||
@@ -88,7 +89,6 @@ class App(MessagePump):
|
||||
self._title = title
|
||||
self._layout = DockLayout()
|
||||
self._view_stack: list[DockView] = []
|
||||
self.children: set[Widget] = set()
|
||||
|
||||
self.focused: Widget | None = None
|
||||
self.mouse_over: Widget | None = None
|
||||
@@ -115,6 +115,8 @@ class App(MessagePump):
|
||||
self.css_file = css_file
|
||||
self.css = css
|
||||
|
||||
self.registry: set[MessagePump] = set()
|
||||
|
||||
super().__init__()
|
||||
|
||||
title: Reactive[str] = Reactive("Textual")
|
||||
@@ -150,6 +152,10 @@ class App(MessagePump):
|
||||
add_children(branch, self.view)
|
||||
return tree
|
||||
|
||||
@property
|
||||
def css_type(self) -> str:
|
||||
return "app"
|
||||
|
||||
def load_css(self, filename: str) -> None:
|
||||
pass
|
||||
|
||||
@@ -340,8 +346,8 @@ class App(MessagePump):
|
||||
self.log_file.close()
|
||||
|
||||
def register(self, child: MessagePump, parent: MessagePump) -> bool:
|
||||
if child not in self.children:
|
||||
self.children.add(child)
|
||||
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))
|
||||
@@ -349,15 +355,15 @@ class App(MessagePump):
|
||||
return False
|
||||
|
||||
def is_mounted(self, widget: Widget) -> bool:
|
||||
return widget in self.children
|
||||
return widget in self.registry
|
||||
|
||||
async def close_all(self) -> None:
|
||||
while self.children:
|
||||
child = self.children.pop()
|
||||
while self.registry:
|
||||
child = self.registry.pop()
|
||||
await child.close_messages()
|
||||
|
||||
async def remove(self, child: MessagePump) -> None:
|
||||
self.children.remove(child)
|
||||
self.registry.remove(child)
|
||||
|
||||
async def shutdown(self):
|
||||
driver = self._driver
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Iterable
|
||||
|
||||
from .styles import Styles
|
||||
from .tokenize import Token
|
||||
from .types import Specificity3
|
||||
|
||||
|
||||
class SelectorType(Enum):
|
||||
@@ -33,18 +34,18 @@ class Location:
|
||||
class Selector:
|
||||
name: str
|
||||
combinator: CombinatorType = CombinatorType.SAME
|
||||
selector: SelectorType = SelectorType.TYPE
|
||||
type: SelectorType = SelectorType.TYPE
|
||||
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
|
||||
def css(self) -> str:
|
||||
psuedo_suffix = "".join(f":{name}" for name in self.pseudo_classes)
|
||||
if self.selector == SelectorType.UNIVERSAL:
|
||||
if self.type == SelectorType.UNIVERSAL:
|
||||
return "*"
|
||||
elif self.selector == SelectorType.TYPE:
|
||||
elif self.type == SelectorType.TYPE:
|
||||
return f"{self.name}{psuedo_suffix}"
|
||||
elif self.selector == SelectorType.CLASS:
|
||||
elif self.type == SelectorType.CLASS:
|
||||
return f".{self.name}{psuedo_suffix}"
|
||||
else:
|
||||
return f"#{self.name}{psuedo_suffix}"
|
||||
@@ -55,14 +56,11 @@ class Declaration:
|
||||
name: str
|
||||
tokens: list[Token] = field(default_factory=list)
|
||||
|
||||
def process(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelectorSet:
|
||||
selectors: list[Selector] = field(default_factory=list)
|
||||
specificity: tuple[int, int, int] = (0, 0, 0)
|
||||
specificity: Specificity3 = (0, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def from_selectors(cls, selectors: list[list[Selector]]) -> Iterable[SelectorSet]:
|
||||
|
||||
@@ -59,7 +59,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
Selector(
|
||||
name=token.value.lstrip(".#"),
|
||||
combinator=combinator,
|
||||
selector=_selector,
|
||||
type=_selector,
|
||||
specificity=specificity,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -113,12 +113,12 @@ class Styles:
|
||||
|
||||
def extract_rules(
|
||||
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__
|
||||
return {
|
||||
rule_name: (
|
||||
getattr(self, rule_name),
|
||||
(int(is_important(rule_name)), *specificity),
|
||||
getattr(self, rule_name),
|
||||
)
|
||||
for rule_name in RULE_NAMES
|
||||
if getattr(self, f"_rule_{rule_name}") is not None
|
||||
|
||||
@@ -2,9 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import rich.repr
|
||||
|
||||
from ..dom import DOMNode
|
||||
from .errors import StylesheetError
|
||||
from .model import RuleSet
|
||||
from .model import CombinatorType, RuleSet, Selector, SelectorType
|
||||
from .parse import parse
|
||||
from .styles import Styles
|
||||
from .types import Specificity3
|
||||
|
||||
from ..widget import Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -37,3 +42,34 @@ class Stylesheet:
|
||||
except Exception as error:
|
||||
raise StylesheetError(f"failed to parse css; {error}") from None
|
||||
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
|
||||
|
||||
@@ -15,3 +15,5 @@ else:
|
||||
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
||||
Display = Literal["block", "none"]
|
||||
EdgeStyle = Tuple[str, Style]
|
||||
Specificity3 = Tuple[int, int, int]
|
||||
Specificity4 = Tuple[int, int, int, int]
|
||||
|
||||
61
src/textual/dom.py
Normal file
61
src/textual/dom.py
Normal 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)
|
||||
@@ -27,14 +27,12 @@ from . import errors
|
||||
from ._animator import BoundAnimator
|
||||
from ._border import Border, BORDER_STYLES
|
||||
from ._callback import invoke
|
||||
from ._widget_list import WidgetList
|
||||
from .dom import DOMNode
|
||||
from ._context import active_app
|
||||
from .geometry import Size, Spacing, SpacingDimensions
|
||||
from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
from .messages import Layout, Update
|
||||
from .reactive import Reactive, watch
|
||||
from .css.styles import Styles
|
||||
from ._types import Lines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -58,18 +56,17 @@ class RenderCache(NamedTuple):
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Widget(MessagePump):
|
||||
class Widget(DOMNode):
|
||||
_counts: ClassVar[dict[str, int]] = {}
|
||||
can_focus: bool = False
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
class_name = self.__class__.__name__
|
||||
Widget._counts.setdefault(class_name, 0)
|
||||
Widget._counts[class_name] += 1
|
||||
_count = self._counts[class_name]
|
||||
|
||||
self.name = name or f"_{class_name}{_count}"
|
||||
self._id = id
|
||||
if name is None:
|
||||
class_name = self.__class__.__name__
|
||||
Widget._counts.setdefault(class_name, 0)
|
||||
Widget._counts[class_name] += 1
|
||||
_count = self._counts[class_name]
|
||||
name = f"{class_name}{_count}"
|
||||
|
||||
self._size = Size(0, 0)
|
||||
self._repaint_required = False
|
||||
@@ -78,12 +75,8 @@ class Widget(MessagePump):
|
||||
self._reactive_watches: dict[str, Callable] = {}
|
||||
self.render_cache: RenderCache | None = None
|
||||
self.highlight_style: Style | None = None
|
||||
self._class_names: set[str] = set()
|
||||
|
||||
self.styles: Styles = Styles()
|
||||
self.children = WidgetList()
|
||||
|
||||
super().__init__()
|
||||
super().__init__(name=name, id=id)
|
||||
|
||||
visible: Reactive[bool] = Reactive(True, layout=True)
|
||||
layout_size: Reactive[int | None] = Reactive(None, layout=True)
|
||||
@@ -144,45 +137,6 @@ class Widget(MessagePump):
|
||||
return widget
|
||||
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:
|
||||
watch(self, attribute_name, callback)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user