mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
refactor of views
This commit is contained in:
@@ -84,7 +84,7 @@ class App(MessagePump):
|
||||
self._title = title
|
||||
self._layout = DockLayout()
|
||||
self._view_stack: list[DockView] = []
|
||||
self.children: set[MessagePump] = set()
|
||||
self.children: set[Widget] = set()
|
||||
|
||||
self.focused: Widget | None = None
|
||||
self.mouse_over: Widget | None = None
|
||||
@@ -327,6 +327,9 @@ class App(MessagePump):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_mounted(self, widget: Widget) -> bool:
|
||||
return widget in self.children
|
||||
|
||||
async def close_all(self) -> None:
|
||||
while self.children:
|
||||
child = self.children.pop()
|
||||
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from .styles import Styles
|
||||
from .styles import Styles, StylesBuilder
|
||||
from .tokenize import Token
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from rich import print
|
||||
|
||||
from typing import Callable, Iterator, Iterable
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
from .tokenize import tokenize, Token
|
||||
|
||||
from .model import Declaration, RuleSet, Selector, CombinatorType, SelectorType
|
||||
from .styles import StylesBuilder
|
||||
|
||||
|
||||
SELECTOR_MAP = {
|
||||
@@ -28,6 +29,8 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
get_selector = SELECTOR_MAP.get
|
||||
combinator = CombinatorType.SAME
|
||||
selectors: list[Selector] = []
|
||||
rule_selectors: list[list[Selector]] = []
|
||||
styles_builder = StylesBuilder()
|
||||
|
||||
while True:
|
||||
if token.name == "pseudo_class":
|
||||
@@ -36,7 +39,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
if combinator == CombinatorType.SAME:
|
||||
combinator = CombinatorType.DESCENDENT
|
||||
elif token.name == "new_selector":
|
||||
rule_set.selectors.append(selectors[:])
|
||||
rule_selectors.append(selectors[:])
|
||||
selectors.clear()
|
||||
combinator = CombinatorType.SAME
|
||||
elif token.name == "declaration_set_start":
|
||||
@@ -54,7 +57,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
token = next(tokens)
|
||||
|
||||
if selectors:
|
||||
rule_set.selectors.append(selectors[:])
|
||||
rule_selectors.append(selectors[:])
|
||||
|
||||
declaration = Declaration("")
|
||||
|
||||
@@ -65,7 +68,7 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
continue
|
||||
if token_name == "declaration_name":
|
||||
if declaration.tokens:
|
||||
rule_set.styles.add_declaration(declaration)
|
||||
styles_builder.add_declaration(declaration)
|
||||
declaration = Declaration("")
|
||||
declaration.name = token.value.rstrip(":")
|
||||
elif token_name == "declaration_set_end":
|
||||
@@ -74,8 +77,9 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
declaration.tokens.append(token)
|
||||
|
||||
if declaration.tokens:
|
||||
rule_set.styles.add_declaration(declaration)
|
||||
styles_builder.add_declaration(declaration)
|
||||
|
||||
rule_set = RuleSet(rule_selectors, styles_builder.styles)
|
||||
yield rule_set
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import cast, Sequence, TYPE_CHECKING
|
||||
from typing import cast, Iterable, Sequence, TYPE_CHECKING
|
||||
|
||||
from rich import print
|
||||
import rich.repr
|
||||
@@ -82,6 +82,9 @@ class Styles:
|
||||
_outline_bottom: tuple[str, Color] | None = None
|
||||
_outline_left: tuple[str, Color] | None = None
|
||||
|
||||
_dock_group: str | None = None
|
||||
_docks: tuple[str, ...] | None = None
|
||||
|
||||
_important: set[str] = field(default_factory=set)
|
||||
|
||||
@property
|
||||
@@ -291,6 +294,33 @@ class Styles:
|
||||
lines.sort()
|
||||
return lines
|
||||
|
||||
@property
|
||||
def dock_group(self) -> str:
|
||||
return self._dock_group or ""
|
||||
|
||||
@dock_group.setter
|
||||
def dock_group(self, name: str | None) -> None:
|
||||
self._dock_group = name
|
||||
|
||||
@property
|
||||
def docks(self) -> tuple[str, ...]:
|
||||
return tuple(self._docks) if self._docks else ()
|
||||
|
||||
@docks.setter
|
||||
def docks(self, docks: str | Iterable[str]) -> None:
|
||||
if isinstance(docks, str):
|
||||
self._docks = tuple(name.lower().strip() for name in docks.split(","))
|
||||
else:
|
||||
self._docks = tuple(docs)
|
||||
|
||||
|
||||
class StylesBuilder:
|
||||
def __init__(self) -> None:
|
||||
self.styles = Styles()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "styles", self.styles
|
||||
|
||||
def error(self, name: str, token: Token, msg: str) -> None:
|
||||
line, col = token.location
|
||||
raise DeclarationError(name, token, f"{msg} (line {line + 1}, col {col + 1})")
|
||||
@@ -311,7 +341,7 @@ class Styles:
|
||||
tokens = declaration.tokens
|
||||
if tokens[-1].name == "important":
|
||||
tokens = tokens[:-1]
|
||||
self._important.add(declaration.name)
|
||||
self.styles._important.add(declaration.name)
|
||||
if process_method is not None:
|
||||
process_method(declaration.name, tokens)
|
||||
|
||||
@@ -321,7 +351,7 @@ class Styles:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_DISPLAY:
|
||||
self._display = cast(Display, value)
|
||||
self.styles._display = cast(Display, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -337,7 +367,7 @@ class Styles:
|
||||
if name == "token":
|
||||
value = value.lower()
|
||||
if value in VALID_VISIBILITY:
|
||||
self._visibility = cast(Visibility, value)
|
||||
self.styles._visibility = cast(Visibility, value)
|
||||
else:
|
||||
self.error(
|
||||
name,
|
||||
@@ -360,7 +390,11 @@ class Styles:
|
||||
self.error(
|
||||
name, tokens[0], f"1, 2, or 4 values expected (received {len(space)})"
|
||||
)
|
||||
setattr(self, f"_{name}", Spacing.unpack(cast(SpacingDimensions, tuple(space))))
|
||||
setattr(
|
||||
self.styles,
|
||||
f"_{name}",
|
||||
Spacing.unpack(cast(SpacingDimensions, tuple(space))),
|
||||
)
|
||||
|
||||
def process_padding(self, name: str, tokens: list[Token]) -> None:
|
||||
self._process_space(name, tokens)
|
||||
@@ -388,12 +422,13 @@ class Styles:
|
||||
|
||||
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("border", tokens)
|
||||
setattr(self, f"_border_{edge}", border)
|
||||
setattr(self.styles, f"_border_{edge}", border)
|
||||
|
||||
def process_border(self, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("border", tokens)
|
||||
self._border_top = self._border_right = border
|
||||
self._border_bottom = self._border_left = border
|
||||
styles = self.styles
|
||||
styles._border_top = styles._border_right = border
|
||||
styles._border_bottom = styles._border_left = border
|
||||
|
||||
def process_border_top(self, name: str, tokens: list[Token]) -> None:
|
||||
self._process_border("top", name, tokens)
|
||||
@@ -409,12 +444,13 @@ class Styles:
|
||||
|
||||
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("outline", tokens)
|
||||
setattr(self, f"_outline_{edge}", border)
|
||||
setattr(self.styles, f"_outline_{edge}", border)
|
||||
|
||||
def process_outline(self, name: str, tokens: list[Token]) -> None:
|
||||
border = self._parse_border("outline", tokens)
|
||||
self._outline_top = self._outline_right = border
|
||||
self._outline_bottom = self._outline_left = border
|
||||
styles = self.styles
|
||||
styles._outline_top = styles._outline_right = border
|
||||
styles._outline_bottom = styles._outline_left = border
|
||||
|
||||
def process_outline_top(self, name: str, tokens: list[Token]) -> None:
|
||||
self._process_outline("top", name, tokens)
|
||||
@@ -431,13 +467,13 @@ class Styles:
|
||||
def process_text(self, name: str, tokens: list[Token]) -> None:
|
||||
style_definition = " ".join(token.value for token in tokens)
|
||||
style = Style.parse(style_definition)
|
||||
self._text = style
|
||||
self.styles._text = style
|
||||
|
||||
def process_text_color(self, name: str, tokens: list[Token]) -> None:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self._text += Style(color=Color.parse(token.value))
|
||||
self.styles._text += Style(color=Color.parse(token.value))
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -451,7 +487,7 @@ class Styles:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self._text += Style(bgcolor=Color.parse(token.value))
|
||||
self.styles._text += Style(bgcolor=Color.parse(token.value))
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
|
||||
@@ -26,7 +26,6 @@ class Stylesheet:
|
||||
try:
|
||||
with open(filename, "rt") as css_file:
|
||||
css = css_file.read()
|
||||
del css_file
|
||||
except Exception as error:
|
||||
raise StylesheetError(f"unable to read {filename!r}; {error}") from None
|
||||
try:
|
||||
|
||||
@@ -8,6 +8,7 @@ import rich.repr
|
||||
from rich.style import Style
|
||||
|
||||
from . import events
|
||||
from . import errors
|
||||
from . import log
|
||||
from . import messages
|
||||
from .layout import Layout, NoWidget, WidgetPlacement
|
||||
@@ -30,7 +31,6 @@ class View(Widget):
|
||||
self.layout: Layout = layout or self.layout_factory()
|
||||
self.mouse_over: Widget | None = None
|
||||
self.widgets: set[Widget] = set()
|
||||
self.named_widgets: dict[str, Widget] = {}
|
||||
self._mouse_style: Style = Style()
|
||||
self._mouse_widget: Widget | None = None
|
||||
|
||||
@@ -72,7 +72,10 @@ class View(Widget):
|
||||
yield "name", self.name
|
||||
|
||||
def __getitem__(self, widget_name: str) -> Widget:
|
||||
return self.named_widgets[widget_name]
|
||||
try:
|
||||
return self.get_child(widget_name)
|
||||
except errors.MissingWidget as error:
|
||||
raise KeyError(str(error))
|
||||
|
||||
@property
|
||||
def is_visual(self) -> bool:
|
||||
@@ -83,7 +86,7 @@ class View(Widget):
|
||||
return bool(self._parent and self.parent is self.app)
|
||||
|
||||
def is_mounted(self, widget: Widget) -> bool:
|
||||
return widget in self.widgets
|
||||
return self.app.is_mounted(widget)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return self.layout
|
||||
@@ -123,11 +126,9 @@ class View(Widget):
|
||||
((None, widget) for widget in anon_widgets), widgets.items()
|
||||
)
|
||||
for name, widget in name_widgets:
|
||||
name = name or widget.name
|
||||
if self.app.register(widget, self):
|
||||
if name:
|
||||
self.named_widgets[name] = widget
|
||||
self.widgets.add(widget)
|
||||
if name is not None:
|
||||
widget.name = name
|
||||
self.add_child(widget)
|
||||
|
||||
self.refresh()
|
||||
|
||||
@@ -255,6 +256,6 @@ class View(Widget):
|
||||
await self.post_message(event)
|
||||
|
||||
async def action_toggle(self, name: str) -> None:
|
||||
widget = self.named_widgets[name]
|
||||
widget = self[name]
|
||||
widget.visible = not widget.visible
|
||||
await self.post_message(messages.Layout(self))
|
||||
|
||||
@@ -23,14 +23,17 @@ from rich.styled import Styled
|
||||
from rich.text import TextType
|
||||
|
||||
from . import events
|
||||
from . import errors
|
||||
from ._animator import BoundAnimator
|
||||
from ._callback import invoke
|
||||
from ._widget_list import WidgetList
|
||||
from ._context import active_app
|
||||
from .geometry import Size, Spacing, SpacingDimensions
|
||||
from .message import Message
|
||||
from .message_pump import MessagePump
|
||||
from .messages import Layout, Update
|
||||
from .reactive import Reactive, watch
|
||||
from .css.styles import Styles
|
||||
from ._types import Lines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -55,17 +58,17 @@ class RenderCache(NamedTuple):
|
||||
|
||||
@rich.repr.auto
|
||||
class Widget(MessagePump):
|
||||
_id: ClassVar[int] = 0
|
||||
_counts: ClassVar[dict[str, int]] = {}
|
||||
can_focus: bool = False
|
||||
|
||||
def __init__(self, name: str | None = None) -> None:
|
||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||
class_name = self.__class__.__name__
|
||||
Widget._counts.setdefault(class_name, 0)
|
||||
Widget._counts[class_name] += 1
|
||||
_count = self._counts[class_name]
|
||||
|
||||
self.name = name or f"{class_name}#{_count}"
|
||||
self._id = id
|
||||
|
||||
self._size = Size(0, 0)
|
||||
self._repaint_required = False
|
||||
@@ -74,9 +77,14 @@ class Widget(MessagePump):
|
||||
self._reactive_watches: dict[str, Callable] = {}
|
||||
self.render_cache: RenderCache | None = None
|
||||
self.highlight_style: Style | None = None
|
||||
self._class_names: set[str] = set()
|
||||
|
||||
self.styles: Styles = Styles()
|
||||
self.children = WidgetList()
|
||||
|
||||
super().__init__()
|
||||
|
||||
id: str | None = None
|
||||
visible: Reactive[bool] = Reactive(True, layout=True)
|
||||
layout_size: Reactive[int | None] = Reactive(None, layout=True)
|
||||
layout_fraction: Reactive[int] = Reactive(1, layout=True)
|
||||
@@ -116,6 +124,56 @@ class Widget(MessagePump):
|
||||
renderable = self.render_styled()
|
||||
return renderable
|
||||
|
||||
def add_child(self, widget: Widget) -> None:
|
||||
"""Add a child widget.
|
||||
|
||||
Args:
|
||||
widget (Widget): Widget
|
||||
"""
|
||||
self.app.register(widget, self)
|
||||
self.children.append(widget)
|
||||
|
||||
def get_child(self, name: str | None = None) -> Widget:
|
||||
for widget in self.children:
|
||||
if widget.name == name:
|
||||
return widget
|
||||
raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}")
|
||||
|
||||
@property
|
||||
def class_names(self) -> frozenset[str]:
|
||||
return frozenset(self._class_names)
|
||||
|
||||
@property
|
||||
def _css_path(self) -> list[tuple[MessagePump, list[Widget]]]:
|
||||
result: list[tuple[MessagePump, list[Widget]]] = []
|
||||
|
||||
# TODO:
|
||||
widget: Widget = self
|
||||
while isinstance(widget._parent, Widget):
|
||||
result.append((widget, widget.children[:]))
|
||||
widget = widget._parent
|
||||
return result[::-1]
|
||||
|
||||
def has_class(self, *class_names: str) -> bool:
|
||||
return self._class_names.issuperset(class_names)
|
||||
|
||||
def add_class(self, *class_names: str) -> None:
|
||||
"""Add class names."""
|
||||
self._class_names.update(class_names)
|
||||
|
||||
def remove_class(self, *class_names: str) -> None:
|
||||
"""Remove class names"""
|
||||
self._class_names.difference_update(class_names)
|
||||
|
||||
def toggle_class(self, *class_names: str) -> None:
|
||||
"""Toggle class names"""
|
||||
_class_names = self._class_names
|
||||
for class_name in class_names:
|
||||
if class_name in _class_names:
|
||||
_class_names.discard(class_name)
|
||||
else:
|
||||
_class_names.add(class_name)
|
||||
|
||||
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
|
||||
watch(self, attribute_name, callback)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user