Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowing none in some CSS rules #4982

Merged
merged 10 commits into from
Sep 12, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Input validation for floats and integers accept embedded underscores, e.g., "1_234_567" is valid. https://github.com/Textualize/textual/pull/4784
- Support for `"none"` value added to `dock`, `hatch` and `split` styles https://github.com/Textualize/textual/pull/4982
- Support for `"none"` added to box and border style properties (e.g `widget.style.border = "none"`) https://github.com/Textualize/textual/pull/4982
- Docstrings added to most style properties https://github.com/Textualize/textual/pull/4982

### Changed

- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
- Default `scrollbar-size-vertical` changed to `2` in inline styles to match Widget default CSS (unlikely to affect users) https://github.com/Textualize/textual/pull/4982

### Fixed

Expand Down
23 changes: 17 additions & 6 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections import defaultdict
from fractions import Fraction
from operator import attrgetter
from typing import TYPE_CHECKING, Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Callable, Iterable, Mapping, Sequence

from ._layout import DockArrangeResult, WidgetPlacement
from ._partition import partition
Expand All @@ -16,7 +16,7 @@
TOP_Z = 2**31 - 1


def _build_dock_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
def _build_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
"""Organize widgets into layers.

Args:
Expand Down Expand Up @@ -47,17 +47,28 @@ def arrange(

placements: list[WidgetPlacement] = []
scroll_spacing = Spacing()
get_dock = attrgetter("styles.dock")
get_split = attrgetter("styles.split")

def _attrgetter_convert_none(attribute: str) -> Callable[[Widget], str | None]:
darrenburns marked this conversation as resolved.
Show resolved Hide resolved
"""Get an attribute of a widget, but return None if the attribute is "none"."""

def getter(widget: Widget):
value = attrgetter(attribute)(widget)
return value if value != "none" else None

return getter

get_dock = _attrgetter_convert_none("styles.dock")
get_split = _attrgetter_convert_none("styles.split")

styles = widget.styles

# Widgets which will be displayed
display_widgets = [child for child in children if child.styles.display != "none"]

# Widgets organized into layers
dock_layers = _build_dock_layers(display_widgets)
layers = _build_layers(display_widgets)

for widgets in dock_layers.values():
for widgets in layers.values():
# Partition widgets in to split widgets and non-split widgets
non_split_widgets, split_widgets = partition(get_split, widgets)
if split_widgets:
Expand Down
2 changes: 1 addition & 1 deletion src/textual/_styles_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def render_line(

def line_post(segments: Iterable[Segment]) -> Iterable[Segment]:
"""Apply effects to segments inside the border."""
if styles.has_rule("hatch"):
if styles.has_rule("hatch") and styles.hatch != "none":
character, color = styles.hatch
if character != " " and color.a > 0:
hatch_style = Style.from_color(
Expand Down
8 changes: 5 additions & 3 deletions src/textual/css/_help_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,17 +457,19 @@ def dock_property_help_text(property_name: str, context: StylingContext) -> Help
return HelpText(
summary=f"Invalid value for [i]{property_name}[/] property",
bullets=[
Bullet("The value must be one of 'top', 'right', 'bottom' or 'left'"),
Bullet(
"The value must be one of 'top', 'right', 'bottom', 'left' or 'none'"
),
*ContextSpecificBullets(
inline=[
Bullet(
"The 'dock' rule aligns a widget relative to the screen.",
"The 'dock' rule attaches a widget to the edge of a container.",
examples=[Example('header.styles.dock = "top"')],
)
],
css=[
Bullet(
"The 'dock' rule aligns a widget relative to the screen.",
"The 'dock' rule attaches a widget to the edge of a container.",
examples=[Example("dock: top")],
)
],
Expand Down
85 changes: 59 additions & 26 deletions src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@
from __future__ import annotations

from operator import attrgetter
from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, Sequence, TypeVar, cast
from typing import (
TYPE_CHECKING,
Generic,
Iterable,
Literal,
NamedTuple,
Sequence,
TypeVar,
cast,
)

import rich.errors
import rich.repr
Expand Down Expand Up @@ -49,13 +58,12 @@
if TYPE_CHECKING:
from ..canvas import CanvasLineType
from .._layout import Layout
from ..widget import Widget
from .styles import StylesBase

from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType

BorderDefinition: TypeAlias = (
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color] | Literal['none']"
)

PropertyGetType = TypeVar("PropertyGetType")
Expand Down Expand Up @@ -294,7 +302,11 @@ def __get__(
"""
return obj.get_rule(self.name) or ("", self._default_color) # type: ignore[return-value]

def __set__(self, obj: StylesBase, border: tuple[EdgeType, str | Color] | None):
def __set__(
self,
obj: StylesBase,
border: tuple[EdgeType, str | Color] | Literal["none"] | None,
):
"""Set the box property.

Args:
Expand All @@ -304,13 +316,14 @@ def __set__(self, obj: StylesBase, border: tuple[EdgeType, str | Color] | None):
``str`` (e.g. ``"blue on #f0f0f0"`` ) or ``Color`` instead.

Raises:
StyleSyntaxError: If the string supplied for the color has invalid syntax.
StyleValueError: If the string supplied for the color is not a valid color.
"""
_rich_traceback_omit = True

if border is None:
if obj.clear_rule(self.name):
obj.refresh(layout=True)
elif border == "none":
obj.set_rule(self.name, ("", obj.get_rule(self.name)[1]))
else:
_type, color = border
if _type in ("none", "hidden"):
Expand Down Expand Up @@ -453,6 +466,16 @@ def check_refresh() -> None:
clear_rule(left)
check_refresh()
return
elif border == "none":
set_rule = obj.set_rule
get_rule = obj.get_rule
set_rule(top, ("", get_rule(top)[1]))
set_rule(right, ("", get_rule(right)[1]))
set_rule(bottom, ("", get_rule(bottom)[1]))
set_rule(left, ("", get_rule(left)[1]))
check_refresh()
return

if isinstance(border, tuple) and len(border) == 2:
_border = normalize_border_value(border) # type: ignore
setattr(obj, top, _border)
Expand Down Expand Up @@ -587,7 +610,7 @@ def __get__(
"""
return obj.get_rule("dock", "") # type: ignore[return-value]

def __set__(self, obj: StylesBase, dock_name: str | None):
def __set__(self, obj: StylesBase, dock_name: str):
"""Set the Dock property.

Args:
Expand All @@ -607,7 +630,7 @@ class SplitProperty:
def __get__(
self, obj: StylesBase, objtype: type[StylesBase] | None = None
) -> DockEdge:
"""Get the Dock property.
"""Get the Split property.

Args:
obj: The ``Styles`` object.
Expand All @@ -618,7 +641,7 @@ def __get__(
"""
return obj.get_rule("split", "") # type: ignore[return-value]

def __set__(self, obj: StylesBase, dock_name: str | None):
def __set__(self, obj: StylesBase, dock_name: str):
"""Set the Dock property.

Args:
Expand Down Expand Up @@ -1170,25 +1193,35 @@ def __set__(
class HatchProperty:
"""Property to expose hatch style."""

def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]:
return obj.get_rule("hatch", (" ", TRANSPARENT)) # type: ignore[return-value]
def __get__(
self, obj: StylesBase, type: type[StylesBase]
) -> tuple[str, Color] | Literal["none"]:
return obj.get_rule("hatch") # type: ignore[return-value]

def __set__(self, obj: StylesBase, value: tuple[str, Color | str] | None) -> None:
def __set__(
self, obj: StylesBase, value: tuple[str, Color | str] | Literal["none"] | None
) -> None:
_rich_traceback_omit = True
if value is None:
obj.clear_rule("hatch")
if obj.clear_rule("hatch"):
obj.refresh(children=True)
return
character, color = value
if len(character) != 1:
try:
character = HATCHES[character]
except KeyError:
raise ValueError(
f"Expected a character or hatch value here; found {character!r}"
) from None
if cell_len(character) != 1:
raise ValueError("Hatch character must have a cell length of 1")
if isinstance(color, str):
color = Color.parse(color)
hatch = (character, color)

if value == "none":
hatch = "none"
else:
character, color = value
if len(character) != 1:
try:
character = HATCHES[character]
except KeyError:
raise ValueError(
f"Expected a character or hatch value here; found {character!r}"
) from None
if cell_len(character) != 1:
raise ValueError("Hatch character must have a cell length of 1")
if isinstance(color, str):
color = Color.parse(color)
hatch = (character, color)

obj.set_rule("hatch", hatch)
14 changes: 9 additions & 5 deletions src/textual/css/_styles_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ def process_keyline(self, name: str, tokens: list[Token]) -> None:
elif token.name == "token":
try:
keyline_color = Color.parse(token.value)
except Exception as error:
except Exception:
keyline_style = token.value
if keyline_style not in VALID_KEYLINE:
self.error(name, token, keyline_help_text())
Expand Down Expand Up @@ -732,8 +732,8 @@ def process_dock(self, name: str, tokens: list[Token]) -> None:
dock_property_help_text(name, context="css"),
)

dock = tokens[0].value
self.styles._rules["dock"] = dock
dock_value = tokens[0].value
self.styles._rules["dock"] = dock_value

def process_split(self, name: str, tokens: list[Token]) -> None:
if not tokens:
Expand All @@ -746,8 +746,8 @@ def process_split(self, name: str, tokens: list[Token]) -> None:
split_property_help_text(name, context="css"),
)

dock = tokens[0].value
self.styles._rules["split"] = dock
split_value = tokens[0].value
self.styles._rules["split"] = split_value

def process_layer(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1:
Expand Down Expand Up @@ -1065,6 +1065,10 @@ def process_hatch(self, name: str, tokens: list[Token]) -> None:
color = TRANSPARENT
opacity = 1.0

if len(tokens) == 1 and tokens[0].value == "none":
self.styles._rules[name] = "none"
return

if len(tokens) not in (2, 3):
self.error(name, tokens[0], "2 or 3 values expected here")

Expand Down
2 changes: 1 addition & 1 deletion src/textual/css/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"vkey",
"wide",
}
VALID_EDGE: Final = {"top", "right", "bottom", "left"}
VALID_EDGE: Final = {"top", "right", "bottom", "left", "none"}
VALID_LAYOUT: Final = {"vertical", "horizontal", "grid"}

VALID_BOX_SIZING: Final = {"border-box", "content-box"}
Expand Down
Loading
Loading