Skip to content

Commit 05fe60d

Browse files
authored
Allowing none in some CSS rules (#4982)
* Allow dock: none; and split: none; * Snapshot test for dock: none * Fix setting border and border_{edge} = "none" in Python (it works in CSS) * Remove print for debugging from styles_cache * Update BorderDefinition typing to allow for "none" like CSS * Docstrings and fixing default `scrollbar_size_vertical` * Remove ToastRack margin-right * Updating CHANGELOG * Adding is_docked and is_split properties to StylesBase, improving docstrings
1 parent 855fb5a commit 05fe60d

File tree

14 files changed

+380
-132
lines changed

14 files changed

+380
-132
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111

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

1518
### Changed
1619

1720
- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
21+
- 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
1822
- Removed border-right from `Toast` https://github.com/Textualize/textual/pull/4984
1923
- Some fixes in `RichLog` result in slightly different semantics, see docstrings for details https://github.com/Textualize/textual/pull/4978
2024

src/textual/_arrange.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
TOP_Z = 2**31 - 1
1717

1818

19-
def _build_dock_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
19+
def _build_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
2020
"""Organize widgets into layers.
2121
2222
Args:
@@ -47,17 +47,19 @@ def arrange(
4747

4848
placements: list[WidgetPlacement] = []
4949
scroll_spacing = Spacing()
50-
get_dock = attrgetter("styles.dock")
51-
get_split = attrgetter("styles.split")
50+
51+
get_dock = attrgetter("styles.is_docked")
52+
get_split = attrgetter("styles.is_split")
53+
5254
styles = widget.styles
5355

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

5759
# Widgets organized into layers
58-
dock_layers = _build_dock_layers(display_widgets)
60+
layers = _build_layers(display_widgets)
5961

60-
for widgets in dock_layers.values():
62+
for widgets in layers.values():
6163
# Partition widgets in to split widgets and non-split widgets
6264
non_split_widgets, split_widgets = partition(get_split, widgets)
6365
if split_widgets:
@@ -162,7 +164,7 @@ def _arrange_dock_widgets(
162164
right = max(right, widget_width)
163165
else:
164166
# Should not occur, mainly to keep Mypy happy
165-
raise AssertionError("invalid value for edge") # pragma: no-cover
167+
raise AssertionError("invalid value for dock edge") # pragma: no-cover
166168

167169
align_offset = dock_widget.styles._align_size(
168170
(widget_width, widget_height), size
@@ -220,6 +222,9 @@ def _arrange_split_widgets(
220222
elif split == "right":
221223
widget_width = int(widget_width_fraction) + margin.width
222224
view_region, split_region = view_region.split_vertical(-widget_width)
225+
else:
226+
raise AssertionError("invalid value for split edge") # pragma: no-cover
227+
223228
append_placement(
224229
_WidgetPlacement(split_region, null_spacing, split_widget, 1, True)
225230
)

src/textual/_styles_cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def render_line(
313313

314314
def line_post(segments: Iterable[Segment]) -> Iterable[Segment]:
315315
"""Apply effects to segments inside the border."""
316-
if styles.has_rule("hatch"):
316+
if styles.has_rule("hatch") and styles.hatch != "none":
317317
character, color = styles.hatch
318318
if character != " " and color.a > 0:
319319
hatch_style = Style.from_color(

src/textual/css/_help_text.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,17 +457,19 @@ def dock_property_help_text(property_name: str, context: StylingContext) -> Help
457457
return HelpText(
458458
summary=f"Invalid value for [i]{property_name}[/] property",
459459
bullets=[
460-
Bullet("The value must be one of 'top', 'right', 'bottom' or 'left'"),
460+
Bullet(
461+
"The value must be one of 'top', 'right', 'bottom', 'left' or 'none'"
462+
),
461463
*ContextSpecificBullets(
462464
inline=[
463465
Bullet(
464-
"The 'dock' rule aligns a widget relative to the screen.",
466+
"The 'dock' rule attaches a widget to the edge of a container.",
465467
examples=[Example('header.styles.dock = "top"')],
466468
)
467469
],
468470
css=[
469471
Bullet(
470-
"The 'dock' rule aligns a widget relative to the screen.",
472+
"The 'dock' rule attaches a widget to the edge of a container.",
471473
examples=[Example("dock: top")],
472474
)
473475
],

src/textual/css/_style_properties.py

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@
99
from __future__ import annotations
1010

1111
from operator import attrgetter
12-
from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, Sequence, TypeVar, cast
12+
from typing import (
13+
TYPE_CHECKING,
14+
Generic,
15+
Iterable,
16+
Literal,
17+
NamedTuple,
18+
Sequence,
19+
TypeVar,
20+
cast,
21+
)
1322

1423
import rich.errors
1524
import rich.repr
@@ -49,13 +58,12 @@
4958
if TYPE_CHECKING:
5059
from ..canvas import CanvasLineType
5160
from .._layout import Layout
52-
from ..widget import Widget
5361
from .styles import StylesBase
5462

5563
from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType
5664

5765
BorderDefinition: TypeAlias = (
58-
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
66+
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color] | Literal['none']"
5967
)
6068

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

297-
def __set__(self, obj: StylesBase, border: tuple[EdgeType, str | Color] | None):
305+
def __set__(
306+
self,
307+
obj: StylesBase,
308+
border: tuple[EdgeType, str | Color] | Literal["none"] | None,
309+
):
298310
"""Set the box property.
299311
300312
Args:
@@ -304,13 +316,14 @@ def __set__(self, obj: StylesBase, border: tuple[EdgeType, str | Color] | None):
304316
``str`` (e.g. ``"blue on #f0f0f0"`` ) or ``Color`` instead.
305317
306318
Raises:
307-
StyleSyntaxError: If the string supplied for the color has invalid syntax.
319+
StyleValueError: If the string supplied for the color is not a valid color.
308320
"""
309-
_rich_traceback_omit = True
310321

311322
if border is None:
312323
if obj.clear_rule(self.name):
313324
obj.refresh(layout=True)
325+
elif border == "none":
326+
obj.set_rule(self.name, ("", obj.get_rule(self.name)[1]))
314327
else:
315328
_type, color = border
316329
if _type in ("none", "hidden"):
@@ -453,6 +466,16 @@ def check_refresh() -> None:
453466
clear_rule(left)
454467
check_refresh()
455468
return
469+
elif border == "none":
470+
set_rule = obj.set_rule
471+
get_rule = obj.get_rule
472+
set_rule(top, ("", get_rule(top)[1]))
473+
set_rule(right, ("", get_rule(right)[1]))
474+
set_rule(bottom, ("", get_rule(bottom)[1]))
475+
set_rule(left, ("", get_rule(left)[1]))
476+
check_refresh()
477+
return
478+
456479
if isinstance(border, tuple) and len(border) == 2:
457480
_border = normalize_border_value(border) # type: ignore
458481
setattr(obj, top, _border)
@@ -583,11 +606,11 @@ def __get__(
583606
objtype: The ``Styles`` class.
584607
585608
Returns:
586-
The dock name as a string, or "" if the rule is not set.
609+
The edge name as a string. Returns "none" if unset or if "none" has been explicitly set.
587610
"""
588-
return obj.get_rule("dock", "") # type: ignore[return-value]
611+
return obj.get_rule("dock", "none") # type: ignore[return-value]
589612

590-
def __set__(self, obj: StylesBase, dock_name: str | None):
613+
def __set__(self, obj: StylesBase, dock_name: str):
591614
"""Set the Dock property.
592615
593616
Args:
@@ -600,25 +623,25 @@ def __set__(self, obj: StylesBase, dock_name: str | None):
600623

601624

602625
class SplitProperty:
603-
"""Descriptor for getting and setting the split property. The split property
604-
allows you to specify which edge you want to split.
626+
"""Descriptor for getting and setting the split property.
627+
The split property allows you to specify which edge you want to split.
605628
"""
606629

607630
def __get__(
608631
self, obj: StylesBase, objtype: type[StylesBase] | None = None
609632
) -> DockEdge:
610-
"""Get the Dock property.
633+
"""Get the Split property.
611634
612635
Args:
613636
obj: The ``Styles`` object.
614637
objtype: The ``Styles`` class.
615638
616639
Returns:
617-
The dock name as a string, or "" if the rule is not set.
640+
The edge name as a string. Returns "none" if unset or if "none" has been explicitly set.
618641
"""
619-
return obj.get_rule("split", "") # type: ignore[return-value]
642+
return obj.get_rule("split", "none") # type: ignore[return-value]
620643

621-
def __set__(self, obj: StylesBase, dock_name: str | None):
644+
def __set__(self, obj: StylesBase, dock_name: str):
622645
"""Set the Dock property.
623646
624647
Args:
@@ -1170,25 +1193,35 @@ def __set__(
11701193
class HatchProperty:
11711194
"""Property to expose hatch style."""
11721195

1173-
def __get__(self, obj: StylesBase, type: type[StylesBase]) -> tuple[str, Color]:
1174-
return obj.get_rule("hatch", (" ", TRANSPARENT)) # type: ignore[return-value]
1196+
def __get__(
1197+
self, obj: StylesBase, type: type[StylesBase]
1198+
) -> tuple[str, Color] | Literal["none"]:
1199+
return obj.get_rule("hatch") # type: ignore[return-value]
11751200

1176-
def __set__(self, obj: StylesBase, value: tuple[str, Color | str] | None) -> None:
1201+
def __set__(
1202+
self, obj: StylesBase, value: tuple[str, Color | str] | Literal["none"] | None
1203+
) -> None:
11771204
_rich_traceback_omit = True
11781205
if value is None:
1179-
obj.clear_rule("hatch")
1206+
if obj.clear_rule("hatch"):
1207+
obj.refresh(children=True)
11801208
return
1181-
character, color = value
1182-
if len(character) != 1:
1183-
try:
1184-
character = HATCHES[character]
1185-
except KeyError:
1186-
raise ValueError(
1187-
f"Expected a character or hatch value here; found {character!r}"
1188-
) from None
1189-
if cell_len(character) != 1:
1190-
raise ValueError("Hatch character must have a cell length of 1")
1191-
if isinstance(color, str):
1192-
color = Color.parse(color)
1193-
hatch = (character, color)
1209+
1210+
if value == "none":
1211+
hatch = "none"
1212+
else:
1213+
character, color = value
1214+
if len(character) != 1:
1215+
try:
1216+
character = HATCHES[character]
1217+
except KeyError:
1218+
raise ValueError(
1219+
f"Expected a character or hatch value here; found {character!r}"
1220+
) from None
1221+
if cell_len(character) != 1:
1222+
raise ValueError("Hatch character must have a cell length of 1")
1223+
if isinstance(color, str):
1224+
color = Color.parse(color)
1225+
hatch = (character, color)
1226+
11941227
obj.set_rule("hatch", hatch)

src/textual/css/_styles_builder.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ def process_keyline(self, name: str, tokens: list[Token]) -> None:
557557
elif token.name == "token":
558558
try:
559559
keyline_color = Color.parse(token.value)
560-
except Exception as error:
560+
except Exception:
561561
keyline_style = token.value
562562
if keyline_style not in VALID_KEYLINE:
563563
self.error(name, token, keyline_help_text())
@@ -732,8 +732,8 @@ def process_dock(self, name: str, tokens: list[Token]) -> None:
732732
dock_property_help_text(name, context="css"),
733733
)
734734

735-
dock = tokens[0].value
736-
self.styles._rules["dock"] = dock
735+
dock_value = tokens[0].value
736+
self.styles._rules["dock"] = dock_value
737737

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

749-
dock = tokens[0].value
750-
self.styles._rules["split"] = dock
749+
split_value = tokens[0].value
750+
self.styles._rules["split"] = split_value
751751

752752
def process_layer(self, name: str, tokens: list[Token]) -> None:
753753
if len(tokens) > 1:
@@ -1065,6 +1065,10 @@ def process_hatch(self, name: str, tokens: list[Token]) -> None:
10651065
color = TRANSPARENT
10661066
opacity = 1.0
10671067

1068+
if len(tokens) == 1 and tokens[0].value == "none":
1069+
self.styles._rules[name] = "none"
1070+
return
1071+
10681072
if len(tokens) not in (2, 3):
10691073
self.error(name, tokens[0], "2 or 3 values expected here")
10701074

src/textual/css/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"vkey",
2727
"wide",
2828
}
29-
VALID_EDGE: Final = {"top", "right", "bottom", "left"}
29+
VALID_EDGE: Final = {"top", "right", "bottom", "left", "none"}
3030
VALID_LAYOUT: Final = {"vertical", "horizontal", "grid"}
3131

3232
VALID_BOX_SIZING: Final = {"border-box", "content-box"}

0 commit comments

Comments
 (0)