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 @@ -11,10 +11,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Added `MaskedInput` widget https://github.com/Textualize/textual/pull/4783
- 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
- Removed border-right from `Toast` https://github.com/Textualize/textual/pull/4984
- Some fixes in `RichLog` result in slightly different semantics, see docstrings for details https://github.com/Textualize/textual/pull/4978

Expand Down
17 changes: 11 additions & 6 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,19 @@ def arrange(

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

get_dock = attrgetter("styles.is_docked")
get_split = attrgetter("styles.is_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 Expand Up @@ -162,7 +164,7 @@ def _arrange_dock_widgets(
right = max(right, widget_width)
else:
# Should not occur, mainly to keep Mypy happy
raise AssertionError("invalid value for edge") # pragma: no-cover
raise AssertionError("invalid value for dock edge") # pragma: no-cover

align_offset = dock_widget.styles._align_size(
(widget_width, widget_height), size
Expand Down Expand Up @@ -220,6 +222,9 @@ def _arrange_split_widgets(
elif split == "right":
widget_width = int(widget_width_fraction) + margin.width
view_region, split_region = view_region.split_vertical(-widget_width)
else:
raise AssertionError("invalid value for split edge") # pragma: no-cover

append_placement(
_WidgetPlacement(split_region, null_spacing, split_widget, 1, True)
)
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
97 changes: 65 additions & 32 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 @@ -583,11 +606,11 @@ def __get__(
objtype: The ``Styles`` class.

Returns:
The dock name as a string, or "" if the rule is not set.
The edge name as a string. Returns "none" if unset or if "none" has been explicitly set.
"""
return obj.get_rule("dock", "") # type: ignore[return-value]
return obj.get_rule("dock", "none") # 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 @@ -600,25 +623,25 @@ def __set__(self, obj: StylesBase, dock_name: str | None):


class SplitProperty:
"""Descriptor for getting and setting the split property. The split property
allows you to specify which edge you want to split.
"""Descriptor for getting and setting the split property.
The split property allows you to specify which edge you want to split.
"""

def __get__(
self, obj: StylesBase, objtype: type[StylesBase] | None = None
) -> DockEdge:
"""Get the Dock property.
"""Get the Split property.

Args:
obj: The ``Styles`` object.
objtype: The ``Styles`` class.

Returns:
The dock name as a string, or "" if the rule is not set.
The edge name as a string. Returns "none" if unset or if "none" has been explicitly set.
"""
return obj.get_rule("split", "") # type: ignore[return-value]
return obj.get_rule("split", "none") # 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