mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
eof
This commit is contained in:
78
src/textual/_widget_list.py
Normal file
78
src/textual/_widget_list.py
Normal 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]
|
||||
@@ -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]}'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
src/textual/errors.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class MissingWidget(Exception):
|
||||
pass
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user