refactor of views

This commit is contained in:
Will McGugan
2021-10-17 18:14:56 +01:00
parent b483155cb3
commit dbb03431e9
7 changed files with 134 additions and 33 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -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:

View File

@@ -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))

View File

@@ -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)