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:
|
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]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
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 ._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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user