mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
docstrings and tidy
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment, SegmentLines
|
||||
from rich.style import Style
|
||||
from rich.style import Style, StyleType
|
||||
|
||||
from .css.types import EdgeStyle
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
"""
|
||||
Style properties are descriptors which allow the Styles object to accept different types when
|
||||
setting attributes. This gives the developer more freedom in how to express style information.
|
||||
|
||||
Descriptors also play nicely with Mypy, which is aware that attributes can have different types
|
||||
when setting and getting.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
|
||||
@@ -125,6 +134,20 @@ class Edges(NamedTuple):
|
||||
if left[0]:
|
||||
yield "left", left
|
||||
|
||||
def spacing(self) -> tuple[int, int, int, int]:
|
||||
"""Get spacing created by borders.
|
||||
|
||||
Returns:
|
||||
tuple[int, int, int, int]: Spacing for top, right, bottom, and left.
|
||||
"""
|
||||
top, right, bottom, left = self
|
||||
return (
|
||||
1 if top[0] else 0,
|
||||
1 if right[0] else 0,
|
||||
1 if bottom[0] else 0,
|
||||
1 if left[0] else 0,
|
||||
)
|
||||
|
||||
|
||||
class BorderProperty:
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"""
|
||||
|
||||
The StylesBuilder object takes tokens parsed from the CSS and converts
|
||||
to the appropriate internal types.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast, Iterable, NoReturn
|
||||
|
||||
import rich.repr
|
||||
from rich.color import ANSI_COLOR_NAMES, Color
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .errors import DeclarationError, StyleValueError
|
||||
from .errors import DeclarationError
|
||||
from ._error_tools import friendly_list
|
||||
from .._easing import EASING
|
||||
from ..geometry import Offset, Spacing, SpacingDimensions
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
from .model import Declaration
|
||||
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
||||
from .styles import DockGroup, Styles
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Space:
|
||||
top: int = 0
|
||||
right: int = 0
|
||||
bottom: int = 0
|
||||
left: int = 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.top}, {self.right}, {self.bottom}, {self.left}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Edge:
|
||||
line: str = "none"
|
||||
style: str = "default"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Border:
|
||||
top: Edge = field(default_factory=Edge)
|
||||
right: Edge = field(default_factory=Edge)
|
||||
bottom: Edge = field(default_factory=Edge)
|
||||
left: Edge = field(default_factory=Edge)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Box:
|
||||
padding: Space = field(default_factory=Space)
|
||||
margin: Space = field(default_factory=Space)
|
||||
border: Border = field(default_factory=Border)
|
||||
outline: Border = field(default_factory=Border)
|
||||
dispay: bool = True
|
||||
visible: bool = True
|
||||
text: str = ""
|
||||
opacity: float = 1.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich import print
|
||||
|
||||
box = Box()
|
||||
print(box)
|
||||
@@ -9,6 +9,15 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool:
|
||||
"""Check if a given selector matches any of the given selector sets.
|
||||
|
||||
Args:
|
||||
selector_sets (Iterable[SelectorSet]): Iterable of selector sets.
|
||||
node (DOMNode): DOM node.
|
||||
|
||||
Returns:
|
||||
bool: True if the node matches the selector, otherwise False.
|
||||
"""
|
||||
return any(
|
||||
_check_selectors(selector_set.selectors, node) for selector_set in selector_sets
|
||||
)
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
"""
|
||||
A DOMQuery is a set of DOM nodes associated with a given CSS selector.
|
||||
|
||||
This set of nodes may be further filtered with the filter method. Additional methods apply
|
||||
actions to the nodes in the query.
|
||||
|
||||
If this sounds like JQuery, a (once) popular JS library, it is no coincidence.
|
||||
|
||||
DOMQuery objects are typically created by Widget.filter method.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -38,6 +51,7 @@ class DOMQuery:
|
||||
return len(self._nodes)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""True if non-empty, otherwise False."""
|
||||
return bool(self._nodes)
|
||||
|
||||
def __iter__(self) -> Iterator[DOMNode]:
|
||||
@@ -47,22 +61,39 @@ class DOMQuery:
|
||||
yield self._nodes
|
||||
|
||||
def filter(self, selector: str) -> DOMQuery:
|
||||
"""Filter this set by the given CSS selector.
|
||||
|
||||
Args:
|
||||
selector (str): A CSS selector.
|
||||
|
||||
Returns:
|
||||
DOMQuery: New DOM Query.
|
||||
"""
|
||||
selector_set = parse_selectors(selector)
|
||||
query = DOMQuery()
|
||||
query._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
|
||||
return query
|
||||
|
||||
def first(self) -> DOMNode:
|
||||
"""Get the first matched node.
|
||||
|
||||
Returns:
|
||||
DOMNode: A DOM Node.
|
||||
"""
|
||||
# TODO: Better response to empty query than an IndexError
|
||||
return self._nodes[0]
|
||||
|
||||
def add_class(self, *class_names: str) -> None:
|
||||
"""Add the given class name(s) to nodes."""
|
||||
for node in self._nodes:
|
||||
node.add_class(*class_names)
|
||||
|
||||
def remove_class(self, *class_names: str) -> None:
|
||||
"""Remove the given class names from the nodes."""
|
||||
for node in self._nodes:
|
||||
node.remove_class(*class_names)
|
||||
|
||||
def toggle_class(self, *class_names: str) -> None:
|
||||
"""Toggle the given class names from matched nodes."""
|
||||
for node in self._nodes:
|
||||
node.toggle_class(*class_names)
|
||||
|
||||
@@ -154,6 +154,16 @@ class Styles:
|
||||
"min_height",
|
||||
}
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
"""Get the gutter (additional space reserved for margin / padding / border).
|
||||
|
||||
Returns:
|
||||
Spacing: [description]
|
||||
"""
|
||||
gutter = self.margin + self.padding + self.border.spacing
|
||||
return gutter
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024)
|
||||
def parse(cls, css: str, path: str) -> Styles:
|
||||
|
||||
@@ -50,6 +50,14 @@ class DOMNode(MessagePump):
|
||||
|
||||
@property
|
||||
def parent(self) -> DOMNode:
|
||||
"""Get the parent node.
|
||||
|
||||
Raises:
|
||||
NoParent: If this is the root node.
|
||||
|
||||
Returns:
|
||||
DOMNode: The node which is the direct parent of this node.
|
||||
"""
|
||||
if self._parent is None:
|
||||
raise NoParent(f"{self} has no parent")
|
||||
assert isinstance(self._parent, DOMNode)
|
||||
@@ -57,10 +65,24 @@ class DOMNode(MessagePump):
|
||||
|
||||
@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.
|
||||
|
||||
"""
|
||||
if self._id is not None:
|
||||
raise ValueError(
|
||||
"Node 'id' attribute may not be changed once set (current id={self._id!r})"
|
||||
@@ -83,10 +105,20 @@ class DOMNode(MessagePump):
|
||||
|
||||
@property
|
||||
def css_type(self) -> str:
|
||||
"""Gets the CSS type, used by the CSS.
|
||||
|
||||
Returns:
|
||||
str: A type used in CSS (lower cased class name).
|
||||
"""
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
@property
|
||||
def css_path(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
|
||||
|
||||
@@ -137,6 +169,11 @@ class DOMNode(MessagePump):
|
||||
|
||||
@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.
|
||||
"""
|
||||
highlighter = ReprHighlighter()
|
||||
tree = Tree(highlighter(repr(self)))
|
||||
|
||||
@@ -163,6 +200,11 @@ class DOMNode(MessagePump):
|
||||
pass
|
||||
|
||||
def add_child(self, node: DOMNode) -> None:
|
||||
"""Add a new child node.
|
||||
|
||||
Args:
|
||||
node (DOMNode): A DOM node.
|
||||
"""
|
||||
self.children._append(node)
|
||||
node.set_parent(self)
|
||||
|
||||
|
||||
@@ -507,5 +507,14 @@ class Spacing(NamedTuple):
|
||||
return cls(top, right, bottom, left)
|
||||
raise ValueError(f"1, 2 or 4 integers required for spacing; {len(pad)} given")
|
||||
|
||||
def __add__(self, other: object) -> Spacing:
|
||||
if isinstance(other, tuple):
|
||||
top1, right1, bottom1, left1 = self
|
||||
top2, right2, bottom2, left2 = other
|
||||
return Spacing(
|
||||
top1 + top2, right1 + right2, bottom1 + bottom2, left1 + left2
|
||||
)
|
||||
return NotImplemented
|
||||
|
||||
|
||||
NULL_OFFSET = Offset(0, 0)
|
||||
|
||||
@@ -84,18 +84,12 @@ class View(Widget):
|
||||
def scroll(self) -> Offset:
|
||||
return Offset(self.scroll_x, self.scroll_y)
|
||||
|
||||
# def __rich_console__(
|
||||
# self, console: Console, options: ConsoleOptions
|
||||
# ) -> RenderResult:
|
||||
# return
|
||||
# yield
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "name", self.name
|
||||
|
||||
def __getitem__(self, widget_name: str) -> Widget:
|
||||
def __getitem__(self, widget_id: str) -> Widget:
|
||||
try:
|
||||
return self.get_child(widget_name)
|
||||
return self.get_child_by_id(widget_id)
|
||||
except errors.MissingWidget as error:
|
||||
raise KeyError(str(error))
|
||||
|
||||
|
||||
@@ -65,12 +65,12 @@ class Widget(DOMNode):
|
||||
"""
|
||||
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
# if name is None:
|
||||
# class_name = self.__class__.__name__
|
||||
# Widget._counts.setdefault(class_name, 0)
|
||||
# Widget._counts[class_name] += 1
|
||||
# _count = self._counts[class_name]
|
||||
# name = f"{class_name}{_count}"
|
||||
if name is None:
|
||||
class_name = self.__class__.__name__
|
||||
Widget._counts.setdefault(class_name, 0)
|
||||
Widget._counts[class_name] += 1
|
||||
_count = self._counts[class_name]
|
||||
name = f"{class_name}{_count}"
|
||||
|
||||
self._size = Size(0, 0)
|
||||
self._repaint_required = False
|
||||
@@ -82,26 +82,6 @@ class Widget(DOMNode):
|
||||
|
||||
super().__init__(name=name, id=id)
|
||||
|
||||
# visible: Reactive[bool] = Reactive(True, layout=True)
|
||||
layout_size: Reactive[int | None] = Reactive(None, layout=True)
|
||||
layout_fraction: Reactive[int] = Reactive(1, layout=True)
|
||||
layout_min_size: Reactive[int] = Reactive(1, layout=True)
|
||||
# layout_offset_x: Reactive[float] = Reactive(0.0, layout=True)
|
||||
# layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
|
||||
|
||||
# style: Reactive[str | None] = Reactive(None)
|
||||
padding: Reactive[Spacing | None] = Reactive(None, layout=True)
|
||||
margin: Reactive[Spacing | None] = Reactive(None, layout=True)
|
||||
border: Reactive[str] = Reactive("none", layout=True)
|
||||
border_style: Reactive[str] = Reactive("green")
|
||||
border_title: Reactive[TextType] = Reactive("")
|
||||
|
||||
def validate_padding(self, padding: SpacingDimensions) -> Spacing:
|
||||
return Spacing.unpack(padding)
|
||||
|
||||
def validate_margin(self, margin: SpacingDimensions) -> Spacing:
|
||||
return Spacing.unpack(margin)
|
||||
|
||||
def __init_subclass__(cls, can_focus: bool = True) -> None:
|
||||
super().__init_subclass__()
|
||||
cls.can_focus = can_focus
|
||||
@@ -117,16 +97,43 @@ class Widget(DOMNode):
|
||||
renderable = self.render_styled()
|
||||
return renderable
|
||||
|
||||
def get_child(self, name: str | None = None, id: str | None = None) -> Widget:
|
||||
if name is not None:
|
||||
for widget in self.children:
|
||||
if widget.name == name:
|
||||
return cast(Widget, widget)
|
||||
if id is not None:
|
||||
for widget in self.children:
|
||||
if widget.id == id:
|
||||
return cast(Widget, widget)
|
||||
raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}")
|
||||
def get_child_by_id(self, id: str) -> Widget:
|
||||
"""Get a child with a given id.
|
||||
|
||||
Args:
|
||||
id (str): A Widget id.
|
||||
|
||||
Raises:
|
||||
errors.MissingWidget: If the widget was not found.
|
||||
|
||||
Returns:
|
||||
Widget: A child widget.
|
||||
"""
|
||||
|
||||
for widget in self.children:
|
||||
if widget.id == id:
|
||||
return cast(Widget, widget)
|
||||
raise errors.MissingWidget(f"Widget with id=={id!r} was not found in {self}")
|
||||
|
||||
def get_child_by_name(self, name: str) -> Widget:
|
||||
"""Get a child widget with a given name.
|
||||
|
||||
Args:
|
||||
name (str): A name. Defaults to None.
|
||||
|
||||
Raises:
|
||||
errors.MissingWidget: If no Widget is found.
|
||||
|
||||
Returns:
|
||||
Widget: A Widget with the given name.
|
||||
"""
|
||||
|
||||
for widget in self.children:
|
||||
if widget.name == name:
|
||||
return cast(Widget, widget)
|
||||
raise errors.MissingWidget(
|
||||
f"Widget with name=={name!r} was not found in {self}"
|
||||
)
|
||||
|
||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||
watch(self, attribute_name, callback)
|
||||
@@ -196,12 +203,12 @@ class Widget(DOMNode):
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
mt, mr, mb, bl = self.margin or (0, 0, 0, 0)
|
||||
pt, pr, pb, pl = self.padding or (0, 0, 0, 0)
|
||||
border = 1 if self.border else 0
|
||||
gutter = Spacing(
|
||||
mt + pt + border, mr + pr + border, mb + pb + border, bl + pl + border
|
||||
)
|
||||
"""Get additional space reserved by margin / padding / border.
|
||||
|
||||
Returns:
|
||||
Spacing: [description]
|
||||
"""
|
||||
gutter = self.styles.gutter
|
||||
return gutter
|
||||
|
||||
def on_style_change(self) -> None:
|
||||
@@ -312,9 +319,20 @@ class Widget(DOMNode):
|
||||
await self.app.set_focus(self)
|
||||
|
||||
async def capture_mouse(self, capture: bool = True) -> None:
|
||||
"""Capture (or release) the mouse.
|
||||
|
||||
When captured, all mouse coordinates will go to this widget even when the pointer is not directly over the widget.
|
||||
|
||||
Args:
|
||||
capture (bool, optional): True to capture or False to release. Defaults to True.
|
||||
"""
|
||||
await self.app.capture_mouse(self if capture else None)
|
||||
|
||||
async def release_mouse(self) -> None:
|
||||
"""Release the mouse.
|
||||
|
||||
Mouse events will only be sent when the mouse is over the widget.
|
||||
"""
|
||||
await self.app.capture_mouse(None)
|
||||
|
||||
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user