This commit is contained in:
Will McGugan
2021-10-17 20:54:11 +01:00
parent dbb03431e9
commit 9fe8904095
11 changed files with 190 additions and 11 deletions

View File

@@ -0,0 +1,78 @@
from __future__ import annotations
from typing import Iterable, overload, TYPE_CHECKING
from weakref import ref
import rich.repr
if TYPE_CHECKING:
from .widget import Widget
@rich.repr.auto
class WidgetList:
"""
A container for widgets that forms one level of hierarchy.
Although named a list, widgets may appear only once, making them more like a set.
"""
def __init__(self) -> None:
self._widget_refs: list[ref[Widget]] = []
self.__widgets: list[Widget] | None = []
def __rich_repr__(self) -> rich.repr.Result:
yield self._widgets
def __len__(self) -> int:
return len(self._widgets)
def __contains__(self, widget: Widget) -> bool:
return widget in self._widgets
@property
def _widgets(self) -> list[Widget]:
if self.__widgets is None:
self.__widgets = list(
filter(None, [widget_ref() for widget_ref in self._widget_refs])
)
return self.__widgets
def _prune(self) -> None:
"""Remove expired references."""
self._widget_refs[:] = filter(
None,
[
None if widget_ref() is None else widget_ref
for widget_ref in self._widget_refs
],
)
def _append(self, widget: Widget):
if widget not in self._widgets:
self._widget_refs.append(ref(widget))
self.__widgets = None
def _clear(self) -> None:
del self._widget_refs[:]
self.__widgets = None
def __iter__(self) -> Iterable[Widget]:
for widget_ref in self._widget_refs:
widget = widget_ref()
if widget is not None:
yield widget
@overload
def __getitem__(self, index: int) -> Widget:
...
@overload
def __getitem__(self, index: slice) -> list[Widget]:
...
def __getitem__(self, index: int | slice) -> Widget | list[Widget]:
self._prune()
assert self._widgets is not None
return self._widgets[index]

View File

@@ -4,9 +4,23 @@ from typing import Iterable
def friendly_list(words: Iterable[str], joiner: str = "or") -> str:
"""Generate a list of words as readable prose.
>>> friendly_list(["foo", "bar", "baz"])
"foo, bar, or baz"
Args:
words (Iterable[str]): A list of words.
joiner (str, optional): The last joiner word. Defaults to "or".
Returns:
str: List as prose.
"""
words = [repr(word) for word in sorted(words, key=str.lower)]
if len(words) == 1:
return words[0]
elif len(words) == 2:
word1, word2 = words
return f"{word1} {joiner} {word2}"
else:
return f'{", ".join(words[:-1])} {joiner} {words[-1]}'
return f'{", ".join(words[:-1])}, {joiner} {words[-1]}'

View File

@@ -107,6 +107,9 @@ if __name__ == "__main__":
text: bold red on magenta
text-color: green;
text-background: white
docks: foo bar bar
dock-group: foo
dock-edge: top
}"""
from .stylesheet import Stylesheet

View File

@@ -23,9 +23,15 @@ class DeclarationError(Exception):
super().__init__(message)
class StyleValueError(ValueError):
pass
VALID_VISIBILITY = {"visible", "hidden"}
VALID_DISPLAY = {"block", "none"}
VALID_BORDER = {"rounded", "solid", "double", "dashed", "heavy", "inner", "outer"}
VALID_EDGE = {"", "top", "right", "bottom", "left"}
VALID_LAYOUT = {"dock", "vertical", "grid"}
NULL_SPACING = Spacing(0, 0, 0, 0)
@@ -69,6 +75,7 @@ class Styles:
_text: Style = Style()
_layout: str = ""
_padding: Spacing | None = None
_margin: Spacing | None = None
@@ -83,6 +90,7 @@ class Styles:
_outline_left: tuple[str, Color] | None = None
_dock_group: str | None = None
_dock_edge: str = ""
_docks: tuple[str, ...] | None = None
_important: set[str] = field(default_factory=set)
@@ -94,7 +102,9 @@ class Styles:
@display.setter
def display(self, display: Display) -> None:
if display not in VALID_DISPLAY:
raise ValueError(f"display must be one of {friendly_list(VALID_DISPLAY)}")
raise StyleValueError(
f"display must be one of {friendly_list(VALID_DISPLAY)}"
)
self._display = display
@property
@@ -104,7 +114,7 @@ class Styles:
@visibility.setter
def visibility(self, visibility: Visibility) -> None:
if visibility not in VALID_VISIBILITY:
raise ValueError(
raise StyleValueError(
f"visibility must be one of {friendly_list(VALID_VISIBILITY)}"
)
self._visibility = visibility
@@ -122,6 +132,18 @@ class Styles:
self._text = _style
return _style
@property
def layout(self) -> str:
return self._layout
@layout.setter
def layout(self, layout: str) -> None:
if layout not in VALID_LAYOUT:
raise StyleValueError(
f"layout must be one of {friendly_list(VALID_LAYOUT)}"
)
self._layout = layout
@property
def padding(self) -> Spacing:
return self._padding or NULL_SPACING
@@ -183,7 +205,7 @@ class Styles:
self.border_bottom = bottom
self.border_left = left
else:
raise ValueError("expected 1, 2, or 4 values")
raise StyleValueError("expected 1, 2, or 4 values")
border_top = _BoxSetter()
border_right = _BoxSetter()
@@ -201,6 +223,9 @@ class Styles:
yield "border_right", self.border_right, None
yield "border_top", self.border_top, None
yield "display", self.display, "block"
yield "dock_edge", self.dock_edge, ""
yield "dock_group", self.dock_group, ""
yield "docks", self.docks, ()
yield "margin", self.margin, NULL_SPACING
yield "outline_bottom", self.outline_bottom, None
yield "outline_left", self.outline_left, None
@@ -291,6 +316,12 @@ class Styles:
_type, color = self._outline_left
append_declaration("outline-left", f"{_type} {color.name}")
if self._dock_group:
append_declaration("dock-group", self._dock_group)
if self._docks:
append_declaration("docks", " ".join(self._docks))
if self._dock_edge:
append_declaration("dock-edge", self._dock_edge)
lines.sort()
return lines
@@ -311,7 +342,17 @@ class Styles:
if isinstance(docks, str):
self._docks = tuple(name.lower().strip() for name in docks.split(","))
else:
self._docks = tuple(docs)
self._docks = tuple(docks)
@property
def dock_edge(self) -> str:
return self._dock_edge
@dock_edge.setter
def dock_edge(self, edge: str) -> str:
if edge not in VALID_EDGE:
raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}")
self._dock_edge = edge
class StylesBuilder:
@@ -497,6 +538,37 @@ class StylesBuilder:
name, token, f"unexpected token {token.value!r} in declaration"
)
def process_dock_group(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1:
self.error(
name,
tokens[1],
f"unexpected tokens in dock-group declaration",
)
self.styles._dock_group = tokens[0].value if tokens else ""
def process_docks(self, name: str, tokens: list[Token]) -> None:
docks: list[str] = []
for token in tokens:
if token.name == "token":
docks.append(token.value)
else:
self.error(
name,
token,
f"unexpected token {token.value!r} in docks declaration",
)
self.styles._docks = tuple(docks)
def process_dock_edge(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1:
self.error(name, tokens[1], f"unexpected tokens in dock-edge declaration")
try:
self.styles.dock_edge = tokens[0].value if tokens else ""
except StyleValueError as error:
self.error(name, tokens[0], str(error))
if __name__ == "__main__":
styles = Styles()
@@ -505,6 +577,7 @@ if __name__ == "__main__":
styles.visibility = "hidden"
styles.border = ("solid", "rgb(10,20,30)")
styles.outline_right = ("solid", "red")
styles.docks = "foo, bar"
from rich import print

2
src/textual/errors.py Normal file
View File

@@ -0,0 +1,2 @@
class MissingWidget(Exception):
pass

View File

@@ -152,7 +152,9 @@ class Layout(ABC):
...
@abstractmethod
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
"""Generate a layout map that defines where on the screen the widgets will be drawn.
Args:

View File

@@ -20,6 +20,7 @@ else:
if TYPE_CHECKING:
from ..widget import Widget
from ..view import View
DockEdge = Literal["top", "right", "bottom", "left"]
@@ -48,7 +49,9 @@ class DockLayout(Layout):
for dock in self.docks:
yield from dock.widgets
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
map: LayoutMap = LayoutMap(size)
width, height = size

View File

@@ -263,7 +263,9 @@ class GridLayout(Layout):
def get_widgets(self) -> Iterable[Widget]:
return self.widgets.keys()
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
"""Generate a map that associates widgets with their location on screen.
Args:

View File

@@ -34,7 +34,9 @@ class VerticalLayout(Layout):
def get_widgets(self) -> Iterable[Widget]:
return self._widgets
def arrange(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
index = 0
width, _height = size
gutter = self.gutter

View File

@@ -98,7 +98,7 @@ class View(Widget):
cached_size, cached_scroll, arrangement = self._cached_arrangement
if cached_size == size and cached_scroll == scroll:
return arrangement
arrangement = list(self.layout.arrange(size, scroll))
arrangement = list(self.layout.arrange(self, size, scroll))
self._cached_arrangement = (size, scroll, arrangement)
return arrangement

View File

@@ -131,7 +131,7 @@ class Widget(MessagePump):
widget (Widget): Widget
"""
self.app.register(widget, self)
self.children.append(widget)
self.children._append(widget)
def get_child(self, name: str | None = None) -> Widget:
for widget in self.children: