Module textual.dom

Expand source code
from __future__ import annotations

import re
import sys
from collections import deque
from inspect import getfile
from typing import (
    TYPE_CHECKING,
    ClassVar,
    Iterable,
    Iterator,
    Type,
    TypeVar,
    cast,
    overload,
)

import rich.repr
from rich.highlighter import ReprHighlighter
from rich.pretty import Pretty
from rich.style import Style
from rich.text import Text
from rich.tree import Tree

from ._context import NoActiveAppError
from ._node_list import NodeList
from .binding import Bindings, BindingType
from .color import BLACK, WHITE, Color
from .css._error_tools import friendly_list
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
from .css.errors import DeclarationError, StyleValueError
from .css.parse import parse_declarations
from .css.query import NoMatches
from .css.styles import RenderStyles, Styles
from .css.tokenize import IDENTIFIER
from .message_pump import MessagePump
from .timer import Timer

if TYPE_CHECKING:
    from .app import App
    from .css.query import DOMQuery
    from .screen import Screen
    from .widget import Widget

if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal

if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:  # pragma: no cover
    from typing_extensions import TypeAlias


_re_identifier = re.compile(IDENTIFIER)


WalkMethod: TypeAlias = Literal["depth", "breadth"]


class BadIdentifier(Exception):
    """raised by check_identifiers."""


def check_identifiers(description: str, *names: str) -> None:
    """Validate identifier and raise an error if it fails.

    Args:
        description (str): Description of where identifier is used for error message.
        names (list[str]): Identifiers to check.

    Returns:
        bool: True if the name is valid.
    """
    match = _re_identifier.match
    for name in names:
        if match(name) is None:
            raise BadIdentifier(
                f"{name!r} is an invalid {description}; "
                "identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number."
            )


class DOMError(Exception):
    pass


class NoScreen(DOMError):
    pass


@rich.repr.auto
class DOMNode(MessagePump):
    """The base class for object that can be in the Textual DOM (App and Widget)"""

    # CSS defaults
    DEFAULT_CSS: ClassVar[str] = ""

    # Default classes argument if not supplied
    DEFAULT_CLASSES: str = ""

    # Virtual DOM nodes
    COMPONENT_CLASSES: ClassVar[set[str]] = set()

    # Mapping of key bindings
    BINDINGS: ClassVar[list[BindingType]] = []

    # True if this node inherits the CSS from the base class.
    _inherit_css: ClassVar[bool] = True
    # List of names of base class (lower cased) that inherit CSS
    _css_type_names: ClassVar[frozenset[str]] = frozenset()

    def __init__(
        self,
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None:
        self._classes = set()
        self._name = name
        self._id = None
        if id is not None:
            self.id = id

        _classes = classes.split() if classes else []
        check_identifiers("class name", *_classes)
        self._classes.update(_classes)

        self.children = NodeList()
        self._css_styles: Styles = Styles(self)
        self._inline_styles: Styles = Styles(self)
        self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
        # A mapping of class names to Styles set in COMPONENT_CLASSES
        self._component_styles: dict[str, RenderStyles] = {}

        self._auto_refresh: float | None = None
        self._auto_refresh_timer: Timer | None = None
        self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
        self._bindings = Bindings(self.BINDINGS)
        self._has_hover_style: bool = False
        self._has_focus_within: bool = False

        super().__init__()

    @property
    def auto_refresh(self) -> float | None:
        return self._auto_refresh

    @auto_refresh.setter
    def auto_refresh(self, interval: float | None) -> None:
        if self._auto_refresh_timer is not None:
            self._auto_refresh_timer.stop_no_wait()
            self._auto_refresh_timer = None
        if interval is not None:
            self._auto_refresh_timer = self.set_interval(
                interval, self._automatic_refresh, name=f"auto refresh {self!r}"
            )
        self._auto_refresh = interval

    def _automatic_refresh(self) -> None:
        """Perform an automatic refresh (set with auto_refresh property)."""
        self.refresh()

    def __init_subclass__(cls, inherit_css: bool = True) -> None:
        super().__init_subclass__()
        cls._inherit_css = inherit_css
        css_type_names: set[str] = set()
        for base in cls._css_bases(cls):
            css_type_names.add(base.__name__.lower())
        cls._css_type_names = frozenset(css_type_names)

    def get_component_styles(self, name: str) -> RenderStyles:
        """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar).

        Args:
            name (str): Name of the component.

        Raises:
            KeyError: If the component class doesn't exist.

        Returns:
            RenderStyles: A Styles object.
        """
        if name not in self._component_styles:
            raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")
        styles = self._component_styles[name]
        return styles

    @property
    def _node_bases(self) -> Iterator[Type[DOMNode]]:
        """Get the DOMNode bases classes (including self.__class__)

        Returns:
            Iterator[Type[DOMNode]]: An iterable of DOMNode classes.
        """
        # Node bases are in reversed order so that the base class is lower priority
        return self._css_bases(self.__class__)

    @classmethod
    def _css_bases(cls, base: Type[DOMNode]) -> Iterator[Type[DOMNode]]:
        """Get the DOMNode base classes, which inherit CSS.

        Args:
            base (Type[DOMNode]): A DOMNode class

        Returns:
            Iterator[Type[DOMNode]]: An iterable of DOMNode classes.
        """
        _class = base
        while True:
            yield _class
            if not _class._inherit_css:
                break
            for _base in _class.__bases__:
                if issubclass(_base, DOMNode):
                    _class = _base
                    break
            else:
                break

    def _post_register(self, app: App) -> None:
        """Called when the widget is registered

        Args:
            app (App): Parent application.
        """

    def __rich_repr__(self) -> rich.repr.Result:
        yield "name", self._name, None
        yield "id", self._id, None
        if self._classes:
            yield "classes", " ".join(self._classes)

    def get_default_css(self) -> list[tuple[str, str, int]]:
        """Gets the CSS for this class and inherited from bases.

        Returns:
            list[tuple[str, str]]: a list of tuples containing (PATH, SOURCE) for this
                and inherited from base classes.
        """

        css_stack: list[tuple[str, str, int]] = []

        def get_path(base: Type[DOMNode]) -> str:
            """Get a path to the DOM Node"""
            try:
                return f"{getfile(base)}:{base.__name__}"
            except TypeError:
                return f"{base.__name__}"

        for tie_breaker, base in enumerate(self._node_bases):
            css = base.DEFAULT_CSS.strip()
            if css:
                css_stack.append((get_path(base), css, -tie_breaker))

        return css_stack

    @property
    def parent(self) -> DOMNode | None:
        """Get the parent node.

        Returns:
            DOMNode | None: The node which is the direct parent of this node.
        """

        return cast("DOMNode | None", self._parent)

    @property
    def screen(self) -> "Screen":
        """Get the screen that this node is contained within. Note that this may not be the currently active screen within the app."""
        # Get the node by looking up a chain of parents
        # Note that self.screen may not be the same as self.app.screen
        from .screen import Screen

        node = self
        while node and not isinstance(node, Screen):
            node = node._parent
        if not isinstance(node, Screen):
            raise NoScreen("node has no screen")
        return node

    @property
    def id(self) -> str | None:
        """The ID of this node, or None if the node has no ID.

        Returns:
            (str | None): A Node ID or None.
        """
        return self._id

    @id.setter
    def id(self, new_id: str) -> str:
        """Sets the ID (may only be done once).

        Args:
            new_id (str): ID for this node.

        Raises:
            ValueError: If the ID has already been set.

        """
        check_identifiers("id", new_id)

        if self._id is not None:
            raise ValueError(
                f"Node 'id' attribute may not be changed once set (current id={self._id!r})"
            )
        self._id = new_id
        return new_id

    @property
    def name(self) -> str | None:
        return self._name

    @property
    def css_identifier(self) -> str:
        """A CSS selector that identifies this DOM node."""
        tokens = [self.__class__.__name__]
        if self.id is not None:
            tokens.append(f"#{self.id}")
        return "".join(tokens)

    @property
    def css_identifier_styled(self) -> Text:
        """A stylized CSS identifier."""
        tokens = Text.styled(self.__class__.__name__)
        if self.id is not None:
            tokens.append(f"#{self.id}", style="bold")
        if self.classes:
            tokens.append(".")
            tokens.append(".".join(class_name for class_name in self.classes), "italic")
        if self.name:
            tokens.append(f"[name={self.name}]", style="underline")
        return tokens

    @property
    def classes(self) -> frozenset[str]:
        """A frozenset of the current classes set on the widget.

        Returns:
            frozenset[str]: Set of class names.

        """
        return frozenset(self._classes)

    @property
    def pseudo_classes(self) -> frozenset[str]:
        """Get a set of all pseudo classes"""
        pseudo_classes = frozenset({*self.get_pseudo_classes()})
        return pseudo_classes

    @property
    def css_path_nodes(self) -> list[DOMNode]:
        """A list of nodes from the root to this node, forming a "path".

        Returns:
            list[DOMNode]: List of Nodes, starting with the root and ending with this node.
        """
        result: list[DOMNode] = [self]
        append = result.append

        node: DOMNode = self
        while isinstance(node._parent, DOMNode):
            node = node._parent
            append(node)
        return result[::-1]

    @property
    def _selector_names(self) -> list[str]:
        """Get a set of selectors applicable to this widget.

        Returns:
            set[str]: Set of selector names.
        """
        selectors: list[str] = [
            "*",
            *(f".{class_name}" for class_name in self._classes),
            *(f":{class_name}" for class_name in self.get_pseudo_classes()),
            *self._css_types,
        ]
        if self._id is not None:
            selectors.append(f"#{self._id}")
        return selectors

    @property
    def display(self) -> bool:
        """
        Check if this widget should display or not.

        Returns:
            bool: ``True`` if this DOMNode is displayed (``display != "none"``) otherwise ``False`` .
        """
        return self.styles.display != "none" and not (self._closing or self._closed)

    @display.setter
    def display(self, new_val: bool | str) -> None:
        """
        Args:
            new_val (bool | str): Shortcut to set the ``display`` CSS property.
                ``False`` will set ``display: none``. ``True`` will set ``display: block``.
                A ``False`` value will prevent the DOMNode from consuming space in the layout.
        """
        # TODO: This will forget what the original "display" value was, so if a user
        #  toggles to False then True, we'll reset to the default "block", rather than
        #  what the user initially specified.
        if isinstance(new_val, bool):
            self.styles.display = "block" if new_val else "none"
        elif new_val in VALID_DISPLAY:
            self.styles.display = new_val
        else:
            raise StyleValueError(
                f"invalid value for display (received {new_val!r}, "
                f"expected {friendly_list(VALID_DISPLAY)})",
            )

    @property
    def visible(self) -> bool:
        """Check if the node is visible or None.

        Returns:
            bool: True if the node is visible.
        """
        return self.styles.visibility != "hidden"

    @visible.setter
    def visible(self, new_value: bool) -> None:
        if isinstance(new_value, bool):
            self.styles.visibility = "visible" if new_value else "hidden"
        elif new_value in VALID_VISIBILITY:
            self.styles.visibility = new_value
        else:
            raise StyleValueError(
                f"invalid value for visibility (received {new_value!r}, "
                f"expected {friendly_list(VALID_VISIBILITY)})"
            )

    @property
    def tree(self) -> Tree:
        """Get a Rich tree object which will recursively render the structure of the node tree.

        Returns:
            Tree: A Rich object which may be printed.
        """
        from rich.columns import Columns
        from rich.console import Group
        from rich.panel import Panel

        from .widget import Widget

        def render_info(node: DOMNode) -> Columns:
            if isinstance(node, Widget):
                info = Columns(
                    [
                        Pretty(node),
                        highlighter(f"region={node.region!r}"),
                        highlighter(
                            f"virtual_size={node.virtual_size!r}",
                        ),
                    ]
                )
            else:
                info = Columns([Pretty(node)])
            return info

        highlighter = ReprHighlighter()
        tree = Tree(render_info(self))

        def add_children(tree, node):
            for child in node.children:
                info = render_info(child)
                css = child.styles.css
                if css:
                    info = Group(
                        info,
                        Panel.fit(
                            Text(child.styles.css),
                            border_style="dim",
                            title="css",
                            title_align="left",
                        ),
                    )
                branch = tree.add(info)
                if tree.children:
                    add_children(branch, child)

        add_children(tree, self)
        return tree

    @property
    def text_style(self) -> Style:
        """Get the text style object.

        A widget's style is influenced by its parent. for instance if a parent is bold, then
        the child will also be bold.

        Returns:
            Style: Rich Style object.
        """
        return Style.combine(
            node.styles.text_style for node in reversed(self.ancestors)
        )

    @property
    def rich_style(self) -> Style:
        """Get a Rich Style object for this DOMNode."""
        background = WHITE
        color = BLACK
        style = Style()
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                background += styles.background
            if styles.has_rule("color"):
                color = styles.color
            style += styles.text_style
            if styles.has_rule("auto_color") and styles.auto_color:
                color = background.get_contrast_text(color.a)
        style += Style.from_color(
            (background + color).rich_color, background.rich_color
        )
        return style

    @property
    def background_colors(self) -> tuple[Color, Color]:
        """Get the background color and the color of the parent's background.

        Returns:
            tuple[Color, Color]: Tuple of (base background, background)

        """
        base_background = background = BLACK
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                base_background = background
                background += styles.background
        return (base_background, background)

    @property
    def colors(self) -> tuple[Color, Color, Color, Color]:
        """Gets the Widgets foreground and background colors, and its parent's (base) colors.

        Returns:
            tuple[Color, Color, Color, Color]: Tuple of (base background, base color, background, color)
        """
        base_background = background = WHITE
        base_color = color = BLACK
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                base_background = background
                background += styles.background
            if styles.has_rule("color"):
                base_color = color
                if styles.auto_color:
                    color = background.get_contrast_text(color.a)
                else:
                    color = styles.color

        return (base_background, base_color, background, color)

    @property
    def ancestors(self) -> list[DOMNode]:
        """Get a list of Nodes by tracing ancestors all the way back to App."""
        nodes: list[MessagePump | None] = []
        add_node = nodes.append
        node: MessagePump | None = self
        while node is not None:
            add_node(node)
            node = node._parent
        return cast("list[DOMNode]", nodes)

    @property
    def displayed_children(self) -> list[Widget]:
        """The children which don't have display: none set.

        Returns:
            list[DOMNode]: Children of this widget which will be displayed.

        """
        return [child for child in self.children if child.display]

    def get_pseudo_classes(self) -> Iterable[str]:
        """Get any pseudo classes applicable to this Node, e.g. hover, focus.

        Returns:
            Iterable[str]: Iterable of strings, such as a generator.
        """
        return ()

    def reset_styles(self) -> None:
        """Reset styles back to their initial state"""
        from .widget import Widget

        for node in self.walk_children():
            node._css_styles.reset()
            if isinstance(node, Widget):
                node._set_dirty()
                node._layout_required = True

    def _add_child(self, node: Widget) -> None:
        """Add a new child node.

        Args:
            node (DOMNode): A DOM node.
        """
        self.children._append(node)
        node._attach(self)

    def _add_children(self, *nodes: Widget, **named_nodes: Widget) -> None:
        """Add multiple children to this node.

        Args:
            *nodes (DOMNode): Positional args should be new DOM nodes.
            **named_nodes (DOMNode): Keyword args will be assigned the argument name as an ID.
        """
        _append = self.children._append
        for node in nodes:
            node._attach(self)
            _append(node)
        for node_id, node in named_nodes.items():
            node._attach(self)
            _append(node)
            node.id = node_id

    WalkType = TypeVar("WalkType")

    @overload
    def walk_children(
        self,
        filter_type: type[WalkType],
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[WalkType]:
        ...

    @overload
    def walk_children(
        self,
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[DOMNode]:
        ...

    def walk_children(
        self,
        filter_type: type[WalkType] | None = None,
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[DOMNode] | list[WalkType]:
        """Generate descendant nodes.

        Args:
            filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
                Defaults to None.
            with_self (bool, optional): Also yield self in addition to descendants. Defaults to True.
            method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "depth".
            reverse (bool, optional): Reverse the order (bottom up). Defaults to False.

        Returns:
            list[DOMNode] | list[WalkType]: A list of nodes.

        """

        def walk_depth_first() -> Iterable[DOMNode]:
            """Walk the tree depth first (parents first)."""
            stack: list[Iterator[DOMNode]] = [iter(self.children)]
            pop = stack.pop
            push = stack.append
            check_type = filter_type or DOMNode

            if with_self and isinstance(self, check_type):
                yield self
            while stack:
                node = next(stack[-1], None)
                if node is None:
                    pop()
                else:
                    if isinstance(node, check_type):
                        yield node
                    if node.children:
                        push(iter(node.children))

        def walk_breadth_first() -> Iterable[DOMNode]:
            """Walk the tree breadth first (children first)."""
            queue: deque[DOMNode] = deque()
            popleft = queue.popleft
            extend = queue.extend
            check_type = filter_type or DOMNode

            if with_self and isinstance(self, check_type):
                yield self
            extend(self.children)
            while queue:
                node = popleft()
                if isinstance(node, check_type):
                    yield node
                extend(node.children)

        node_generator = (
            walk_depth_first() if method == "depth" else walk_breadth_first()
        )

        # We want a snapshot of the DOM at this point So that it doesn't
        # change mid-walk
        nodes = list(node_generator)
        if reverse:
            nodes.reverse()
        return nodes

    def get_child(self, id: str) -> DOMNode:
        """Return the first child (immediate descendent) of this node with the given ID.

        Args:
            id (str): The ID of the child.

        Returns:
            DOMNode: The first child of this node with the ID.

        Raises:
            NoMatches: if no children could be found for this ID
        """
        for child in self.children:
            if child.id == id:
                return child
        raise NoMatches(f"No child found with id={id!r}")

    ExpectType = TypeVar("ExpectType", bound="Widget")

    @overload
    def query(self, selector: str | None) -> DOMQuery[Widget]:
        ...

    @overload
    def query(self, selector: type[ExpectType]) -> DOMQuery[ExpectType]:
        ...

    def query(
        self, selector: str | type[ExpectType] | None = None
    ) -> DOMQuery[Widget] | DOMQuery[ExpectType]:
        """Get a DOM query matching a selector.

        Args:
            selector (str | type | None, optional): A CSS selector or `None` for all nodes. Defaults to None.

        Returns:
            DOMQuery: A query object.
        """
        from .css.query import DOMQuery

        query: str | None
        if isinstance(selector, str) or selector is None:
            query = selector
        else:
            query = selector.__name__

        return DOMQuery(self, filter=query)

    @overload
    def query_one(self, selector: str) -> Widget:
        ...

    @overload
    def query_one(self, selector: type[ExpectType]) -> ExpectType:
        ...

    @overload
    def query_one(self, selector: str, expect_type: type[ExpectType]) -> ExpectType:
        ...

    def query_one(
        self,
        selector: str | type[ExpectType],
        expect_type: type[ExpectType] | None = None,
    ) -> ExpectType | Widget:
        """Get the first Widget matching the given selector or selector type.

        Args:
            selector (str | type): A selector.
            expect_type (type | None, optional): Require the object be of the supplied type, or None for any type.
                Defaults to None.

        Returns:
            Widget | ExpectType: A widget matching the selector.
        """
        from .css.query import DOMQuery

        if isinstance(selector, str):
            query_selector = selector
        else:
            query_selector = selector.__name__
        query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)

        if expect_type is None:
            return query.first()
        else:
            return query.first(expect_type)

    def set_styles(self, css: str | None = None, **update_styles) -> None:
        """Set custom styles on this object."""

        if css is not None:
            try:
                new_styles = parse_declarations(css, path="set_styles")
            except DeclarationError as error:
                raise DeclarationError(error.name, error.token, error.message) from None
            self._inline_styles.merge(new_styles)
            self.refresh(layout=True)

        styles = self.styles
        for key, value in update_styles.items():
            setattr(styles, key, value)

    def has_class(self, *class_names: str) -> bool:
        """Check if the Node has all the given class names.

        Args:
            *class_names (str): CSS class names to check.

        Returns:
            bool: ``True`` if the node has all the given class names, otherwise ``False``.
        """
        return self._classes.issuperset(class_names)

    def set_class(self, add: bool, *class_names: str) -> None:
        """Add or remove class(es) based on a condition.

        Args:
            add (bool):  Add the classes if True, otherwise remove them.
        """
        if add:
            self.add_class(*class_names)
        else:
            self.remove_class(*class_names)

    def add_class(self, *class_names: str) -> None:
        """Add class names to this Node.

        Args:
            *class_names (str): CSS class names to add.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def remove_class(self, *class_names: str) -> None:
        """Remove class names from this Node.

        Args:
            *class_names (str): CSS class names to remove.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.difference_update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def toggle_class(self, *class_names: str) -> None:
        """Toggle class names on this Node.

        Args:
            *class_names (str): CSS class names to toggle.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.symmetric_difference_update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def has_pseudo_class(self, *class_names: str) -> bool:
        """Check for pseudo class (such as hover, focus etc)"""
        has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
        return has_pseudo_classes

    def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
        pass

Functions

def check_identifiers(description: str, *names: str) ‑> None

Validate identifier and raise an error if it fails.

Args

description : str
Description of where identifier is used for error message.
names : list[str]
Identifiers to check.

Returns

bool
True if the name is valid.
Expand source code
def check_identifiers(description: str, *names: str) -> None:
    """Validate identifier and raise an error if it fails.

    Args:
        description (str): Description of where identifier is used for error message.
        names (list[str]): Identifiers to check.

    Returns:
        bool: True if the name is valid.
    """
    match = _re_identifier.match
    for name in names:
        if match(name) is None:
            raise BadIdentifier(
                f"{name!r} is an invalid {description}; "
                "identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number."
            )

Classes

class BadIdentifier (*args, **kwargs)

raised by check_identifiers.

Expand source code
class BadIdentifier(Exception):
    """raised by check_identifiers."""

Ancestors

  • builtins.Exception
  • builtins.BaseException
class DOMError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class DOMError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class DOMNode (*, name: str | None = None, id: str | None = None, classes: str | None = None)

The base class for object that can be in the Textual DOM (App and Widget)

Expand source code
class DOMNode(MessagePump):
    """The base class for object that can be in the Textual DOM (App and Widget)"""

    # CSS defaults
    DEFAULT_CSS: ClassVar[str] = ""

    # Default classes argument if not supplied
    DEFAULT_CLASSES: str = ""

    # Virtual DOM nodes
    COMPONENT_CLASSES: ClassVar[set[str]] = set()

    # Mapping of key bindings
    BINDINGS: ClassVar[list[BindingType]] = []

    # True if this node inherits the CSS from the base class.
    _inherit_css: ClassVar[bool] = True
    # List of names of base class (lower cased) that inherit CSS
    _css_type_names: ClassVar[frozenset[str]] = frozenset()

    def __init__(
        self,
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None:
        self._classes = set()
        self._name = name
        self._id = None
        if id is not None:
            self.id = id

        _classes = classes.split() if classes else []
        check_identifiers("class name", *_classes)
        self._classes.update(_classes)

        self.children = NodeList()
        self._css_styles: Styles = Styles(self)
        self._inline_styles: Styles = Styles(self)
        self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
        # A mapping of class names to Styles set in COMPONENT_CLASSES
        self._component_styles: dict[str, RenderStyles] = {}

        self._auto_refresh: float | None = None
        self._auto_refresh_timer: Timer | None = None
        self._css_types = {cls.__name__ for cls in self._css_bases(self.__class__)}
        self._bindings = Bindings(self.BINDINGS)
        self._has_hover_style: bool = False
        self._has_focus_within: bool = False

        super().__init__()

    @property
    def auto_refresh(self) -> float | None:
        return self._auto_refresh

    @auto_refresh.setter
    def auto_refresh(self, interval: float | None) -> None:
        if self._auto_refresh_timer is not None:
            self._auto_refresh_timer.stop_no_wait()
            self._auto_refresh_timer = None
        if interval is not None:
            self._auto_refresh_timer = self.set_interval(
                interval, self._automatic_refresh, name=f"auto refresh {self!r}"
            )
        self._auto_refresh = interval

    def _automatic_refresh(self) -> None:
        """Perform an automatic refresh (set with auto_refresh property)."""
        self.refresh()

    def __init_subclass__(cls, inherit_css: bool = True) -> None:
        super().__init_subclass__()
        cls._inherit_css = inherit_css
        css_type_names: set[str] = set()
        for base in cls._css_bases(cls):
            css_type_names.add(base.__name__.lower())
        cls._css_type_names = frozenset(css_type_names)

    def get_component_styles(self, name: str) -> RenderStyles:
        """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar).

        Args:
            name (str): Name of the component.

        Raises:
            KeyError: If the component class doesn't exist.

        Returns:
            RenderStyles: A Styles object.
        """
        if name not in self._component_styles:
            raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")
        styles = self._component_styles[name]
        return styles

    @property
    def _node_bases(self) -> Iterator[Type[DOMNode]]:
        """Get the DOMNode bases classes (including self.__class__)

        Returns:
            Iterator[Type[DOMNode]]: An iterable of DOMNode classes.
        """
        # Node bases are in reversed order so that the base class is lower priority
        return self._css_bases(self.__class__)

    @classmethod
    def _css_bases(cls, base: Type[DOMNode]) -> Iterator[Type[DOMNode]]:
        """Get the DOMNode base classes, which inherit CSS.

        Args:
            base (Type[DOMNode]): A DOMNode class

        Returns:
            Iterator[Type[DOMNode]]: An iterable of DOMNode classes.
        """
        _class = base
        while True:
            yield _class
            if not _class._inherit_css:
                break
            for _base in _class.__bases__:
                if issubclass(_base, DOMNode):
                    _class = _base
                    break
            else:
                break

    def _post_register(self, app: App) -> None:
        """Called when the widget is registered

        Args:
            app (App): Parent application.
        """

    def __rich_repr__(self) -> rich.repr.Result:
        yield "name", self._name, None
        yield "id", self._id, None
        if self._classes:
            yield "classes", " ".join(self._classes)

    def get_default_css(self) -> list[tuple[str, str, int]]:
        """Gets the CSS for this class and inherited from bases.

        Returns:
            list[tuple[str, str]]: a list of tuples containing (PATH, SOURCE) for this
                and inherited from base classes.
        """

        css_stack: list[tuple[str, str, int]] = []

        def get_path(base: Type[DOMNode]) -> str:
            """Get a path to the DOM Node"""
            try:
                return f"{getfile(base)}:{base.__name__}"
            except TypeError:
                return f"{base.__name__}"

        for tie_breaker, base in enumerate(self._node_bases):
            css = base.DEFAULT_CSS.strip()
            if css:
                css_stack.append((get_path(base), css, -tie_breaker))

        return css_stack

    @property
    def parent(self) -> DOMNode | None:
        """Get the parent node.

        Returns:
            DOMNode | None: The node which is the direct parent of this node.
        """

        return cast("DOMNode | None", self._parent)

    @property
    def screen(self) -> "Screen":
        """Get the screen that this node is contained within. Note that this may not be the currently active screen within the app."""
        # Get the node by looking up a chain of parents
        # Note that self.screen may not be the same as self.app.screen
        from .screen import Screen

        node = self
        while node and not isinstance(node, Screen):
            node = node._parent
        if not isinstance(node, Screen):
            raise NoScreen("node has no screen")
        return node

    @property
    def id(self) -> str | None:
        """The ID of this node, or None if the node has no ID.

        Returns:
            (str | None): A Node ID or None.
        """
        return self._id

    @id.setter
    def id(self, new_id: str) -> str:
        """Sets the ID (may only be done once).

        Args:
            new_id (str): ID for this node.

        Raises:
            ValueError: If the ID has already been set.

        """
        check_identifiers("id", new_id)

        if self._id is not None:
            raise ValueError(
                f"Node 'id' attribute may not be changed once set (current id={self._id!r})"
            )
        self._id = new_id
        return new_id

    @property
    def name(self) -> str | None:
        return self._name

    @property
    def css_identifier(self) -> str:
        """A CSS selector that identifies this DOM node."""
        tokens = [self.__class__.__name__]
        if self.id is not None:
            tokens.append(f"#{self.id}")
        return "".join(tokens)

    @property
    def css_identifier_styled(self) -> Text:
        """A stylized CSS identifier."""
        tokens = Text.styled(self.__class__.__name__)
        if self.id is not None:
            tokens.append(f"#{self.id}", style="bold")
        if self.classes:
            tokens.append(".")
            tokens.append(".".join(class_name for class_name in self.classes), "italic")
        if self.name:
            tokens.append(f"[name={self.name}]", style="underline")
        return tokens

    @property
    def classes(self) -> frozenset[str]:
        """A frozenset of the current classes set on the widget.

        Returns:
            frozenset[str]: Set of class names.

        """
        return frozenset(self._classes)

    @property
    def pseudo_classes(self) -> frozenset[str]:
        """Get a set of all pseudo classes"""
        pseudo_classes = frozenset({*self.get_pseudo_classes()})
        return pseudo_classes

    @property
    def css_path_nodes(self) -> list[DOMNode]:
        """A list of nodes from the root to this node, forming a "path".

        Returns:
            list[DOMNode]: List of Nodes, starting with the root and ending with this node.
        """
        result: list[DOMNode] = [self]
        append = result.append

        node: DOMNode = self
        while isinstance(node._parent, DOMNode):
            node = node._parent
            append(node)
        return result[::-1]

    @property
    def _selector_names(self) -> list[str]:
        """Get a set of selectors applicable to this widget.

        Returns:
            set[str]: Set of selector names.
        """
        selectors: list[str] = [
            "*",
            *(f".{class_name}" for class_name in self._classes),
            *(f":{class_name}" for class_name in self.get_pseudo_classes()),
            *self._css_types,
        ]
        if self._id is not None:
            selectors.append(f"#{self._id}")
        return selectors

    @property
    def display(self) -> bool:
        """
        Check if this widget should display or not.

        Returns:
            bool: ``True`` if this DOMNode is displayed (``display != "none"``) otherwise ``False`` .
        """
        return self.styles.display != "none" and not (self._closing or self._closed)

    @display.setter
    def display(self, new_val: bool | str) -> None:
        """
        Args:
            new_val (bool | str): Shortcut to set the ``display`` CSS property.
                ``False`` will set ``display: none``. ``True`` will set ``display: block``.
                A ``False`` value will prevent the DOMNode from consuming space in the layout.
        """
        # TODO: This will forget what the original "display" value was, so if a user
        #  toggles to False then True, we'll reset to the default "block", rather than
        #  what the user initially specified.
        if isinstance(new_val, bool):
            self.styles.display = "block" if new_val else "none"
        elif new_val in VALID_DISPLAY:
            self.styles.display = new_val
        else:
            raise StyleValueError(
                f"invalid value for display (received {new_val!r}, "
                f"expected {friendly_list(VALID_DISPLAY)})",
            )

    @property
    def visible(self) -> bool:
        """Check if the node is visible or None.

        Returns:
            bool: True if the node is visible.
        """
        return self.styles.visibility != "hidden"

    @visible.setter
    def visible(self, new_value: bool) -> None:
        if isinstance(new_value, bool):
            self.styles.visibility = "visible" if new_value else "hidden"
        elif new_value in VALID_VISIBILITY:
            self.styles.visibility = new_value
        else:
            raise StyleValueError(
                f"invalid value for visibility (received {new_value!r}, "
                f"expected {friendly_list(VALID_VISIBILITY)})"
            )

    @property
    def tree(self) -> Tree:
        """Get a Rich tree object which will recursively render the structure of the node tree.

        Returns:
            Tree: A Rich object which may be printed.
        """
        from rich.columns import Columns
        from rich.console import Group
        from rich.panel import Panel

        from .widget import Widget

        def render_info(node: DOMNode) -> Columns:
            if isinstance(node, Widget):
                info = Columns(
                    [
                        Pretty(node),
                        highlighter(f"region={node.region!r}"),
                        highlighter(
                            f"virtual_size={node.virtual_size!r}",
                        ),
                    ]
                )
            else:
                info = Columns([Pretty(node)])
            return info

        highlighter = ReprHighlighter()
        tree = Tree(render_info(self))

        def add_children(tree, node):
            for child in node.children:
                info = render_info(child)
                css = child.styles.css
                if css:
                    info = Group(
                        info,
                        Panel.fit(
                            Text(child.styles.css),
                            border_style="dim",
                            title="css",
                            title_align="left",
                        ),
                    )
                branch = tree.add(info)
                if tree.children:
                    add_children(branch, child)

        add_children(tree, self)
        return tree

    @property
    def text_style(self) -> Style:
        """Get the text style object.

        A widget's style is influenced by its parent. for instance if a parent is bold, then
        the child will also be bold.

        Returns:
            Style: Rich Style object.
        """
        return Style.combine(
            node.styles.text_style for node in reversed(self.ancestors)
        )

    @property
    def rich_style(self) -> Style:
        """Get a Rich Style object for this DOMNode."""
        background = WHITE
        color = BLACK
        style = Style()
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                background += styles.background
            if styles.has_rule("color"):
                color = styles.color
            style += styles.text_style
            if styles.has_rule("auto_color") and styles.auto_color:
                color = background.get_contrast_text(color.a)
        style += Style.from_color(
            (background + color).rich_color, background.rich_color
        )
        return style

    @property
    def background_colors(self) -> tuple[Color, Color]:
        """Get the background color and the color of the parent's background.

        Returns:
            tuple[Color, Color]: Tuple of (base background, background)

        """
        base_background = background = BLACK
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                base_background = background
                background += styles.background
        return (base_background, background)

    @property
    def colors(self) -> tuple[Color, Color, Color, Color]:
        """Gets the Widgets foreground and background colors, and its parent's (base) colors.

        Returns:
            tuple[Color, Color, Color, Color]: Tuple of (base background, base color, background, color)
        """
        base_background = background = WHITE
        base_color = color = BLACK
        for node in reversed(self.ancestors):
            styles = node.styles
            if styles.has_rule("background"):
                base_background = background
                background += styles.background
            if styles.has_rule("color"):
                base_color = color
                if styles.auto_color:
                    color = background.get_contrast_text(color.a)
                else:
                    color = styles.color

        return (base_background, base_color, background, color)

    @property
    def ancestors(self) -> list[DOMNode]:
        """Get a list of Nodes by tracing ancestors all the way back to App."""
        nodes: list[MessagePump | None] = []
        add_node = nodes.append
        node: MessagePump | None = self
        while node is not None:
            add_node(node)
            node = node._parent
        return cast("list[DOMNode]", nodes)

    @property
    def displayed_children(self) -> list[Widget]:
        """The children which don't have display: none set.

        Returns:
            list[DOMNode]: Children of this widget which will be displayed.

        """
        return [child for child in self.children if child.display]

    def get_pseudo_classes(self) -> Iterable[str]:
        """Get any pseudo classes applicable to this Node, e.g. hover, focus.

        Returns:
            Iterable[str]: Iterable of strings, such as a generator.
        """
        return ()

    def reset_styles(self) -> None:
        """Reset styles back to their initial state"""
        from .widget import Widget

        for node in self.walk_children():
            node._css_styles.reset()
            if isinstance(node, Widget):
                node._set_dirty()
                node._layout_required = True

    def _add_child(self, node: Widget) -> None:
        """Add a new child node.

        Args:
            node (DOMNode): A DOM node.
        """
        self.children._append(node)
        node._attach(self)

    def _add_children(self, *nodes: Widget, **named_nodes: Widget) -> None:
        """Add multiple children to this node.

        Args:
            *nodes (DOMNode): Positional args should be new DOM nodes.
            **named_nodes (DOMNode): Keyword args will be assigned the argument name as an ID.
        """
        _append = self.children._append
        for node in nodes:
            node._attach(self)
            _append(node)
        for node_id, node in named_nodes.items():
            node._attach(self)
            _append(node)
            node.id = node_id

    WalkType = TypeVar("WalkType")

    @overload
    def walk_children(
        self,
        filter_type: type[WalkType],
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[WalkType]:
        ...

    @overload
    def walk_children(
        self,
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[DOMNode]:
        ...

    def walk_children(
        self,
        filter_type: type[WalkType] | None = None,
        *,
        with_self: bool = True,
        method: WalkMethod = "depth",
        reverse: bool = False,
    ) -> list[DOMNode] | list[WalkType]:
        """Generate descendant nodes.

        Args:
            filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
                Defaults to None.
            with_self (bool, optional): Also yield self in addition to descendants. Defaults to True.
            method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "depth".
            reverse (bool, optional): Reverse the order (bottom up). Defaults to False.

        Returns:
            list[DOMNode] | list[WalkType]: A list of nodes.

        """

        def walk_depth_first() -> Iterable[DOMNode]:
            """Walk the tree depth first (parents first)."""
            stack: list[Iterator[DOMNode]] = [iter(self.children)]
            pop = stack.pop
            push = stack.append
            check_type = filter_type or DOMNode

            if with_self and isinstance(self, check_type):
                yield self
            while stack:
                node = next(stack[-1], None)
                if node is None:
                    pop()
                else:
                    if isinstance(node, check_type):
                        yield node
                    if node.children:
                        push(iter(node.children))

        def walk_breadth_first() -> Iterable[DOMNode]:
            """Walk the tree breadth first (children first)."""
            queue: deque[DOMNode] = deque()
            popleft = queue.popleft
            extend = queue.extend
            check_type = filter_type or DOMNode

            if with_self and isinstance(self, check_type):
                yield self
            extend(self.children)
            while queue:
                node = popleft()
                if isinstance(node, check_type):
                    yield node
                extend(node.children)

        node_generator = (
            walk_depth_first() if method == "depth" else walk_breadth_first()
        )

        # We want a snapshot of the DOM at this point So that it doesn't
        # change mid-walk
        nodes = list(node_generator)
        if reverse:
            nodes.reverse()
        return nodes

    def get_child(self, id: str) -> DOMNode:
        """Return the first child (immediate descendent) of this node with the given ID.

        Args:
            id (str): The ID of the child.

        Returns:
            DOMNode: The first child of this node with the ID.

        Raises:
            NoMatches: if no children could be found for this ID
        """
        for child in self.children:
            if child.id == id:
                return child
        raise NoMatches(f"No child found with id={id!r}")

    ExpectType = TypeVar("ExpectType", bound="Widget")

    @overload
    def query(self, selector: str | None) -> DOMQuery[Widget]:
        ...

    @overload
    def query(self, selector: type[ExpectType]) -> DOMQuery[ExpectType]:
        ...

    def query(
        self, selector: str | type[ExpectType] | None = None
    ) -> DOMQuery[Widget] | DOMQuery[ExpectType]:
        """Get a DOM query matching a selector.

        Args:
            selector (str | type | None, optional): A CSS selector or `None` for all nodes. Defaults to None.

        Returns:
            DOMQuery: A query object.
        """
        from .css.query import DOMQuery

        query: str | None
        if isinstance(selector, str) or selector is None:
            query = selector
        else:
            query = selector.__name__

        return DOMQuery(self, filter=query)

    @overload
    def query_one(self, selector: str) -> Widget:
        ...

    @overload
    def query_one(self, selector: type[ExpectType]) -> ExpectType:
        ...

    @overload
    def query_one(self, selector: str, expect_type: type[ExpectType]) -> ExpectType:
        ...

    def query_one(
        self,
        selector: str | type[ExpectType],
        expect_type: type[ExpectType] | None = None,
    ) -> ExpectType | Widget:
        """Get the first Widget matching the given selector or selector type.

        Args:
            selector (str | type): A selector.
            expect_type (type | None, optional): Require the object be of the supplied type, or None for any type.
                Defaults to None.

        Returns:
            Widget | ExpectType: A widget matching the selector.
        """
        from .css.query import DOMQuery

        if isinstance(selector, str):
            query_selector = selector
        else:
            query_selector = selector.__name__
        query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)

        if expect_type is None:
            return query.first()
        else:
            return query.first(expect_type)

    def set_styles(self, css: str | None = None, **update_styles) -> None:
        """Set custom styles on this object."""

        if css is not None:
            try:
                new_styles = parse_declarations(css, path="set_styles")
            except DeclarationError as error:
                raise DeclarationError(error.name, error.token, error.message) from None
            self._inline_styles.merge(new_styles)
            self.refresh(layout=True)

        styles = self.styles
        for key, value in update_styles.items():
            setattr(styles, key, value)

    def has_class(self, *class_names: str) -> bool:
        """Check if the Node has all the given class names.

        Args:
            *class_names (str): CSS class names to check.

        Returns:
            bool: ``True`` if the node has all the given class names, otherwise ``False``.
        """
        return self._classes.issuperset(class_names)

    def set_class(self, add: bool, *class_names: str) -> None:
        """Add or remove class(es) based on a condition.

        Args:
            add (bool):  Add the classes if True, otherwise remove them.
        """
        if add:
            self.add_class(*class_names)
        else:
            self.remove_class(*class_names)

    def add_class(self, *class_names: str) -> None:
        """Add class names to this Node.

        Args:
            *class_names (str): CSS class names to add.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def remove_class(self, *class_names: str) -> None:
        """Remove class names from this Node.

        Args:
            *class_names (str): CSS class names to remove.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.difference_update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def toggle_class(self, *class_names: str) -> None:
        """Toggle class names on this Node.

        Args:
            *class_names (str): CSS class names to toggle.

        """
        check_identifiers("class name", *class_names)
        old_classes = self._classes.copy()
        self._classes.symmetric_difference_update(class_names)
        if old_classes == self._classes:
            return
        try:
            self.app.update_styles(self)
        except NoActiveAppError:
            pass

    def has_pseudo_class(self, *class_names: str) -> bool:
        """Check for pseudo class (such as hover, focus etc)"""
        has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
        return has_pseudo_classes

    def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
        pass

Ancestors

Subclasses

Class variables

var BINDINGS : ClassVar[list[BindingType]]
var COMPONENT_CLASSES : ClassVar[set[str]]
var DEFAULT_CLASSES : str
var DEFAULT_CSS : ClassVar[str]
var ExpectType
var WalkType

Instance variables

var ancestors : list[DOMNode]

Get a list of Nodes by tracing ancestors all the way back to App.

Expand source code
@property
def ancestors(self) -> list[DOMNode]:
    """Get a list of Nodes by tracing ancestors all the way back to App."""
    nodes: list[MessagePump | None] = []
    add_node = nodes.append
    node: MessagePump | None = self
    while node is not None:
        add_node(node)
        node = node._parent
    return cast("list[DOMNode]", nodes)
var auto_refresh : float | None
Expand source code
@property
def auto_refresh(self) -> float | None:
    return self._auto_refresh
var background_colors : tuple[Color, Color]

Get the background color and the color of the parent's background.

Returns

tuple[Color, Color]
Tuple of (base background, background)
Expand source code
@property
def background_colors(self) -> tuple[Color, Color]:
    """Get the background color and the color of the parent's background.

    Returns:
        tuple[Color, Color]: Tuple of (base background, background)

    """
    base_background = background = BLACK
    for node in reversed(self.ancestors):
        styles = node.styles
        if styles.has_rule("background"):
            base_background = background
            background += styles.background
    return (base_background, background)
var classes : frozenset[str]

A frozenset of the current classes set on the widget.

Returns

frozenset[str]
Set of class names.
Expand source code
@property
def classes(self) -> frozenset[str]:
    """A frozenset of the current classes set on the widget.

    Returns:
        frozenset[str]: Set of class names.

    """
    return frozenset(self._classes)
var colors : tuple[Color, Color, Color, Color]

Gets the Widgets foreground and background colors, and its parent's (base) colors.

Returns

tuple[Color, Color, Color, Color]
Tuple of (base background, base color, background, color)
Expand source code
@property
def colors(self) -> tuple[Color, Color, Color, Color]:
    """Gets the Widgets foreground and background colors, and its parent's (base) colors.

    Returns:
        tuple[Color, Color, Color, Color]: Tuple of (base background, base color, background, color)
    """
    base_background = background = WHITE
    base_color = color = BLACK
    for node in reversed(self.ancestors):
        styles = node.styles
        if styles.has_rule("background"):
            base_background = background
            background += styles.background
        if styles.has_rule("color"):
            base_color = color
            if styles.auto_color:
                color = background.get_contrast_text(color.a)
            else:
                color = styles.color

    return (base_background, base_color, background, color)
var css_identifier : str

A CSS selector that identifies this DOM node.

Expand source code
@property
def css_identifier(self) -> str:
    """A CSS selector that identifies this DOM node."""
    tokens = [self.__class__.__name__]
    if self.id is not None:
        tokens.append(f"#{self.id}")
    return "".join(tokens)
var css_identifier_styled : rich.text.Text

A stylized CSS identifier.

Expand source code
@property
def css_identifier_styled(self) -> Text:
    """A stylized CSS identifier."""
    tokens = Text.styled(self.__class__.__name__)
    if self.id is not None:
        tokens.append(f"#{self.id}", style="bold")
    if self.classes:
        tokens.append(".")
        tokens.append(".".join(class_name for class_name in self.classes), "italic")
    if self.name:
        tokens.append(f"[name={self.name}]", style="underline")
    return tokens
var css_path_nodes : list[DOMNode]

A list of nodes from the root to this node, forming a "path".

Returns

list[DOMNode]
List of Nodes, starting with the root and ending with this node.
Expand source code
@property
def css_path_nodes(self) -> list[DOMNode]:
    """A list of nodes from the root to this node, forming a "path".

    Returns:
        list[DOMNode]: List of Nodes, starting with the root and ending with this node.
    """
    result: list[DOMNode] = [self]
    append = result.append

    node: DOMNode = self
    while isinstance(node._parent, DOMNode):
        node = node._parent
        append(node)
    return result[::-1]
var display : bool

Check if this widget should display or not.

Returns

bool
True if this DOMNode is displayed (display != "none") otherwise False .
Expand source code
@property
def display(self) -> bool:
    """
    Check if this widget should display or not.

    Returns:
        bool: ``True`` if this DOMNode is displayed (``display != "none"``) otherwise ``False`` .
    """
    return self.styles.display != "none" and not (self._closing or self._closed)
var displayed_children : list[Widget]

The children which don't have display: none set.

Returns

list[DOMNode]
Children of this widget which will be displayed.
Expand source code
@property
def displayed_children(self) -> list[Widget]:
    """The children which don't have display: none set.

    Returns:
        list[DOMNode]: Children of this widget which will be displayed.

    """
    return [child for child in self.children if child.display]
var id : str | None

The ID of this node, or None if the node has no ID.

Returns

(str | None): A Node ID or None.

Expand source code
@property
def id(self) -> str | None:
    """The ID of this node, or None if the node has no ID.

    Returns:
        (str | None): A Node ID or None.
    """
    return self._id
var name : str | None
Expand source code
@property
def name(self) -> str | None:
    return self._name
var parentDOMNode | None

Get the parent node.

Returns

DOMNode | None: The node which is the direct parent of this node.

Expand source code
@property
def parent(self) -> DOMNode | None:
    """Get the parent node.

    Returns:
        DOMNode | None: The node which is the direct parent of this node.
    """

    return cast("DOMNode | None", self._parent)
var pseudo_classes : frozenset[str]

Get a set of all pseudo classes

Expand source code
@property
def pseudo_classes(self) -> frozenset[str]:
    """Get a set of all pseudo classes"""
    pseudo_classes = frozenset({*self.get_pseudo_classes()})
    return pseudo_classes
var rich_style : rich.style.Style

Get a Rich Style object for this DOMNode.

Expand source code
@property
def rich_style(self) -> Style:
    """Get a Rich Style object for this DOMNode."""
    background = WHITE
    color = BLACK
    style = Style()
    for node in reversed(self.ancestors):
        styles = node.styles
        if styles.has_rule("background"):
            background += styles.background
        if styles.has_rule("color"):
            color = styles.color
        style += styles.text_style
        if styles.has_rule("auto_color") and styles.auto_color:
            color = background.get_contrast_text(color.a)
    style += Style.from_color(
        (background + color).rich_color, background.rich_color
    )
    return style
var screen : Screen

Get the screen that this node is contained within. Note that this may not be the currently active screen within the app.

Expand source code
@property
def screen(self) -> "Screen":
    """Get the screen that this node is contained within. Note that this may not be the currently active screen within the app."""
    # Get the node by looking up a chain of parents
    # Note that self.screen may not be the same as self.app.screen
    from .screen import Screen

    node = self
    while node and not isinstance(node, Screen):
        node = node._parent
    if not isinstance(node, Screen):
        raise NoScreen("node has no screen")
    return node
var text_style : rich.style.Style

Get the text style object.

A widget's style is influenced by its parent. for instance if a parent is bold, then the child will also be bold.

Returns

Style
Rich Style object.
Expand source code
@property
def text_style(self) -> Style:
    """Get the text style object.

    A widget's style is influenced by its parent. for instance if a parent is bold, then
    the child will also be bold.

    Returns:
        Style: Rich Style object.
    """
    return Style.combine(
        node.styles.text_style for node in reversed(self.ancestors)
    )
var tree : rich.tree.Tree

Get a Rich tree object which will recursively render the structure of the node tree.

Returns

Tree
A Rich object which may be printed.
Expand source code
@property
def tree(self) -> Tree:
    """Get a Rich tree object which will recursively render the structure of the node tree.

    Returns:
        Tree: A Rich object which may be printed.
    """
    from rich.columns import Columns
    from rich.console import Group
    from rich.panel import Panel

    from .widget import Widget

    def render_info(node: DOMNode) -> Columns:
        if isinstance(node, Widget):
            info = Columns(
                [
                    Pretty(node),
                    highlighter(f"region={node.region!r}"),
                    highlighter(
                        f"virtual_size={node.virtual_size!r}",
                    ),
                ]
            )
        else:
            info = Columns([Pretty(node)])
        return info

    highlighter = ReprHighlighter()
    tree = Tree(render_info(self))

    def add_children(tree, node):
        for child in node.children:
            info = render_info(child)
            css = child.styles.css
            if css:
                info = Group(
                    info,
                    Panel.fit(
                        Text(child.styles.css),
                        border_style="dim",
                        title="css",
                        title_align="left",
                    ),
                )
            branch = tree.add(info)
            if tree.children:
                add_children(branch, child)

    add_children(tree, self)
    return tree
var visible : bool

Check if the node is visible or None.

Returns

bool
True if the node is visible.
Expand source code
@property
def visible(self) -> bool:
    """Check if the node is visible or None.

    Returns:
        bool: True if the node is visible.
    """
    return self.styles.visibility != "hidden"

Methods

def add_class(self, *class_names: str) ‑> None

Add class names to this Node.

Args

*class_names : str
CSS class names to add.
Expand source code
def add_class(self, *class_names: str) -> None:
    """Add class names to this Node.

    Args:
        *class_names (str): CSS class names to add.

    """
    check_identifiers("class name", *class_names)
    old_classes = self._classes.copy()
    self._classes.update(class_names)
    if old_classes == self._classes:
        return
    try:
        self.app.update_styles(self)
    except NoActiveAppError:
        pass
def get_child(self, id: str) ‑> DOMNode

Return the first child (immediate descendent) of this node with the given ID.

Args

id : str
The ID of the child.

Returns

DOMNode
The first child of this node with the ID.

Raises

NoMatches
if no children could be found for this ID
Expand source code
def get_child(self, id: str) -> DOMNode:
    """Return the first child (immediate descendent) of this node with the given ID.

    Args:
        id (str): The ID of the child.

    Returns:
        DOMNode: The first child of this node with the ID.

    Raises:
        NoMatches: if no children could be found for this ID
    """
    for child in self.children:
        if child.id == id:
            return child
    raise NoMatches(f"No child found with id={id!r}")
def get_component_styles(self, name: str) ‑> RenderStyles

Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar).

Args

name : str
Name of the component.

Raises

KeyError
If the component class doesn't exist.

Returns

RenderStyles
A Styles object.
Expand source code
def get_component_styles(self, name: str) -> RenderStyles:
    """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar).

    Args:
        name (str): Name of the component.

    Raises:
        KeyError: If the component class doesn't exist.

    Returns:
        RenderStyles: A Styles object.
    """
    if name not in self._component_styles:
        raise KeyError(f"No {name!r} key in COMPONENT_CLASSES")
    styles = self._component_styles[name]
    return styles
def get_default_css(self) ‑> list[tuple[str, str, int]]

Gets the CSS for this class and inherited from bases.

Returns

list[tuple[str, str]]
a list of tuples containing (PATH, SOURCE) for this and inherited from base classes.
Expand source code
def get_default_css(self) -> list[tuple[str, str, int]]:
    """Gets the CSS for this class and inherited from bases.

    Returns:
        list[tuple[str, str]]: a list of tuples containing (PATH, SOURCE) for this
            and inherited from base classes.
    """

    css_stack: list[tuple[str, str, int]] = []

    def get_path(base: Type[DOMNode]) -> str:
        """Get a path to the DOM Node"""
        try:
            return f"{getfile(base)}:{base.__name__}"
        except TypeError:
            return f"{base.__name__}"

    for tie_breaker, base in enumerate(self._node_bases):
        css = base.DEFAULT_CSS.strip()
        if css:
            css_stack.append((get_path(base), css, -tie_breaker))

    return css_stack
def get_pseudo_classes(self) ‑> Iterable[str]

Get any pseudo classes applicable to this Node, e.g. hover, focus.

Returns

Iterable[str]
Iterable of strings, such as a generator.
Expand source code
def get_pseudo_classes(self) -> Iterable[str]:
    """Get any pseudo classes applicable to this Node, e.g. hover, focus.

    Returns:
        Iterable[str]: Iterable of strings, such as a generator.
    """
    return ()
def has_class(self, *class_names: str) ‑> bool

Check if the Node has all the given class names.

Args

*class_names : str
CSS class names to check.

Returns

bool
True if the node has all the given class names, otherwise False.
Expand source code
def has_class(self, *class_names: str) -> bool:
    """Check if the Node has all the given class names.

    Args:
        *class_names (str): CSS class names to check.

    Returns:
        bool: ``True`` if the node has all the given class names, otherwise ``False``.
    """
    return self._classes.issuperset(class_names)
def has_pseudo_class(self, *class_names: str) ‑> bool

Check for pseudo class (such as hover, focus etc)

Expand source code
def has_pseudo_class(self, *class_names: str) -> bool:
    """Check for pseudo class (such as hover, focus etc)"""
    has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
    return has_pseudo_classes
def query(self, selector: str | type[ExpectType] | None = None) ‑> DOMQuery[Widget] | DOMQuery[ExpectType]

Get a DOM query matching a selector.

Args

selector (str | type | None, optional): A CSS selector or None for all nodes. Defaults to None.

Returns

DOMQuery
A query object.
Expand source code
def query(
    self, selector: str | type[ExpectType] | None = None
) -> DOMQuery[Widget] | DOMQuery[ExpectType]:
    """Get a DOM query matching a selector.

    Args:
        selector (str | type | None, optional): A CSS selector or `None` for all nodes. Defaults to None.

    Returns:
        DOMQuery: A query object.
    """
    from .css.query import DOMQuery

    query: str | None
    if isinstance(selector, str) or selector is None:
        query = selector
    else:
        query = selector.__name__

    return DOMQuery(self, filter=query)
def query_one(self, selector: str | type[ExpectType], expect_type: type[ExpectType] | None = None) ‑> ExpectType | Widget

Get the first Widget matching the given selector or selector type.

Args

selector (str | type): A selector. expect_type (type | None, optional): Require the object be of the supplied type, or None for any type. Defaults to None.

Returns

Widget | ExpectType: A widget matching the selector.

Expand source code
def query_one(
    self,
    selector: str | type[ExpectType],
    expect_type: type[ExpectType] | None = None,
) -> ExpectType | Widget:
    """Get the first Widget matching the given selector or selector type.

    Args:
        selector (str | type): A selector.
        expect_type (type | None, optional): Require the object be of the supplied type, or None for any type.
            Defaults to None.

    Returns:
        Widget | ExpectType: A widget matching the selector.
    """
    from .css.query import DOMQuery

    if isinstance(selector, str):
        query_selector = selector
    else:
        query_selector = selector.__name__
    query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)

    if expect_type is None:
        return query.first()
    else:
        return query.first(expect_type)
def refresh(self, *, repaint: bool = True, layout: bool = False) ‑> None
Expand source code
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
    pass
def remove_class(self, *class_names: str) ‑> None

Remove class names from this Node.

Args

*class_names : str
CSS class names to remove.
Expand source code
def remove_class(self, *class_names: str) -> None:
    """Remove class names from this Node.

    Args:
        *class_names (str): CSS class names to remove.

    """
    check_identifiers("class name", *class_names)
    old_classes = self._classes.copy()
    self._classes.difference_update(class_names)
    if old_classes == self._classes:
        return
    try:
        self.app.update_styles(self)
    except NoActiveAppError:
        pass
def reset_styles(self) ‑> None

Reset styles back to their initial state

Expand source code
def reset_styles(self) -> None:
    """Reset styles back to their initial state"""
    from .widget import Widget

    for node in self.walk_children():
        node._css_styles.reset()
        if isinstance(node, Widget):
            node._set_dirty()
            node._layout_required = True
def set_class(self, add: bool, *class_names: str) ‑> None

Add or remove class(es) based on a condition.

Args

add : bool
Add the classes if True, otherwise remove them.
Expand source code
def set_class(self, add: bool, *class_names: str) -> None:
    """Add or remove class(es) based on a condition.

    Args:
        add (bool):  Add the classes if True, otherwise remove them.
    """
    if add:
        self.add_class(*class_names)
    else:
        self.remove_class(*class_names)
def set_styles(self, css: str | None = None, **update_styles) ‑> None

Set custom styles on this object.

Expand source code
def set_styles(self, css: str | None = None, **update_styles) -> None:
    """Set custom styles on this object."""

    if css is not None:
        try:
            new_styles = parse_declarations(css, path="set_styles")
        except DeclarationError as error:
            raise DeclarationError(error.name, error.token, error.message) from None
        self._inline_styles.merge(new_styles)
        self.refresh(layout=True)

    styles = self.styles
    for key, value in update_styles.items():
        setattr(styles, key, value)
def toggle_class(self, *class_names: str) ‑> None

Toggle class names on this Node.

Args

*class_names : str
CSS class names to toggle.
Expand source code
def toggle_class(self, *class_names: str) -> None:
    """Toggle class names on this Node.

    Args:
        *class_names (str): CSS class names to toggle.

    """
    check_identifiers("class name", *class_names)
    old_classes = self._classes.copy()
    self._classes.symmetric_difference_update(class_names)
    if old_classes == self._classes:
        return
    try:
        self.app.update_styles(self)
    except NoActiveAppError:
        pass
def walk_children(self, filter_type: type[WalkType] | None = None, *, with_self: bool = True, method: WalkMethod = 'depth', reverse: bool = False) ‑> list[DOMNode] | list[WalkType]

Generate descendant nodes.

Args

filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
Defaults to None.
with_self : bool, optional
Also yield self in addition to descendants. Defaults to True.
method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "depth".
reverse : bool, optional
Reverse the order (bottom up). Defaults to False.

Returns

list[DOMNode] | list[WalkType]: A list of nodes.

Expand source code
def walk_children(
    self,
    filter_type: type[WalkType] | None = None,
    *,
    with_self: bool = True,
    method: WalkMethod = "depth",
    reverse: bool = False,
) -> list[DOMNode] | list[WalkType]:
    """Generate descendant nodes.

    Args:
        filter_type (type[WalkType] | None, optional): Filter only this type, or None for no filter.
            Defaults to None.
        with_self (bool, optional): Also yield self in addition to descendants. Defaults to True.
        method (Literal["breadth", "depth"], optional): One of "depth" or "breadth". Defaults to "depth".
        reverse (bool, optional): Reverse the order (bottom up). Defaults to False.

    Returns:
        list[DOMNode] | list[WalkType]: A list of nodes.

    """

    def walk_depth_first() -> Iterable[DOMNode]:
        """Walk the tree depth first (parents first)."""
        stack: list[Iterator[DOMNode]] = [iter(self.children)]
        pop = stack.pop
        push = stack.append
        check_type = filter_type or DOMNode

        if with_self and isinstance(self, check_type):
            yield self
        while stack:
            node = next(stack[-1], None)
            if node is None:
                pop()
            else:
                if isinstance(node, check_type):
                    yield node
                if node.children:
                    push(iter(node.children))

    def walk_breadth_first() -> Iterable[DOMNode]:
        """Walk the tree breadth first (children first)."""
        queue: deque[DOMNode] = deque()
        popleft = queue.popleft
        extend = queue.extend
        check_type = filter_type or DOMNode

        if with_self and isinstance(self, check_type):
            yield self
        extend(self.children)
        while queue:
            node = popleft()
            if isinstance(node, check_type):
                yield node
            extend(node.children)

    node_generator = (
        walk_depth_first() if method == "depth" else walk_breadth_first()
    )

    # We want a snapshot of the DOM at this point So that it doesn't
    # change mid-walk
    nodes = list(node_generator)
    if reverse:
        nodes.reverse()
    return nodes

Inherited members

class NoScreen (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class NoScreen(DOMError):
    pass

Ancestors

  • DOMError
  • builtins.Exception
  • builtins.BaseException