diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02bb52ad7f..8e9199b3e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py
index 52b1ea1c85..b68aa022b3 100644
--- a/src/textual/_arrange.py
+++ b/src/textual/_arrange.py
@@ -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:
@@ -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:
@@ -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
@@ -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)
)
diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py
index 7029b6b4f4..aef80584bf 100644
--- a/src/textual/_styles_cache.py
+++ b/src/textual/_styles_cache.py
@@ -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(
diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py
index da811bad73..55f184203f 100644
--- a/src/textual/css/_help_text.py
+++ b/src/textual/css/_help_text.py
@@ -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")],
)
],
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
index 00cad8c05e..16cbd47d07 100644
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -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
@@ -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")
@@ -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:
@@ -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"):
@@ -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)
@@ -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:
@@ -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:
@@ -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)
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 96cc8e2b9a..7a2146e708 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -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())
@@ -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:
@@ -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:
@@ -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")
diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py
index 07e2523989..fe9c0fd73d 100644
--- a/src/textual/css/constants.py
+++ b/src/textual/css/constants.py
@@ -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"}
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
index 8fe230bc75..7479529f5b 100644
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass, field
from functools import partial
from operator import attrgetter
-from typing import TYPE_CHECKING, Any, Callable, Iterable, cast
+from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, cast
import rich.repr
from rich.style import Style
@@ -187,7 +187,7 @@ class RulesMap(TypedDict, total=False):
border_subtitle_background: Color
border_subtitle_style: Style
- hatch: tuple[str, Color]
+ hatch: tuple[str, Color] | Literal["none"]
overlay: Overlay
constrain: Constrain
@@ -234,75 +234,165 @@ class StylesBase:
display = StringEnumProperty(
VALID_DISPLAY, "block", layout=True, refresh_parent=True, refresh_children=True
)
+ """Set the display of the widget, defining how it's rendered.
+
+ Valid values are "block" or "none".
+
+ "none" will hide and allow other widgets to fill the space that this widget would occupy.
+
+ Set to None to clear any value that was set at runtime.
+
+ Raises:
+ StyleValueError: If an invalid display is specified.
+ """
+
visibility = StringEnumProperty(
VALID_VISIBILITY, "visible", layout=True, refresh_parent=True
)
+ """Set the visibility of the widget.
+
+ Valid values are "visible" or "hidden".
+
+ "hidden" will hide the widget, but reserve the space for this widget.
+ If you want to hide the widget and allow another widget to fill the space,
+ set the display attribute to "none" instead.
+
+ Set to None to clear any value that was set at runtime.
+
+ Raises:
+ StyleValueError: If an invalid visibility is specified.
+ """
+
layout = LayoutProperty()
+ """Set the layout of the widget, defining how it's children are laid out.
+
+ Valid values are "grid", "horizontal", and "vertical" or None to clear any layout
+ that was set at runtime.
+
+ Raises:
+ MissingLayout: If an invalid layout is specified.
+ """
auto_color = BooleanProperty(default=False)
color = ColorProperty(Color(255, 255, 255))
+ """Set the foreground (text) color of the widget.
+ Supports `Color` objects but also strings e.g. "red" or "#ff0000".
+ You can also specify an opacity after a color e.g. "blue 10%"
+ """
background = ColorProperty(Color(0, 0, 0, 0))
+ """Set the background color of the widget.
+ Supports `Color` objects but also strings e.g. "red" or "#ff0000"
+ You can also specify an opacity after a color e.g. "blue 10%"
+ """
text_style = StyleFlagsProperty()
-
+ """Set the text style of the widget using Rich StyleFlags.
+ e.g. `"bold underline"` or `"b u strikethrough"`.
+ """
opacity = FractionalProperty(children=True)
+ """Set the opacity of the widget, defining how it blends with the parent."""
text_opacity = FractionalProperty()
-
+ """Set the opacity of the content within the widget against the widget's background."""
padding = SpacingProperty()
+ """Set the padding (spacing between border and content) of the widget."""
margin = SpacingProperty()
+ """Set the margin (spacing outside the border) of the widget."""
offset = OffsetProperty()
-
+ """Set the offset of the widget relative to where it would have been otherwise."""
border = BorderProperty(layout=True)
+ """Set the border of the widget e.g. ("rounded", "green") or "none"."""
+
border_top = BoxProperty(Color(0, 255, 0))
+ """Set the top border of the widget e.g. ("rounded", "green") or "none"."""
border_right = BoxProperty(Color(0, 255, 0))
+ """Set the right border of the widget e.g. ("rounded", "green") or "none"."""
border_bottom = BoxProperty(Color(0, 255, 0))
+ """Set the bottom border of the widget e.g. ("rounded", "green") or "none"."""
border_left = BoxProperty(Color(0, 255, 0))
+ """Set the left border of the widget e.g. ("rounded", "green") or "none"."""
border_title_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
border_subtitle_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "right")
outline = BorderProperty(layout=False)
+ """Set the outline of the widget e.g. ("rounded", "green") or "none".
+ The outline is drawn *on top* of the widget, rather than around it like border.
+ """
outline_top = BoxProperty(Color(0, 255, 0))
+ """Set the top outline of the widget e.g. ("rounded", "green") or "none"."""
outline_right = BoxProperty(Color(0, 255, 0))
+ """Set the right outline of the widget e.g. ("rounded", "green") or "none"."""
outline_bottom = BoxProperty(Color(0, 255, 0))
+ """Set the bottom outline of the widget e.g. ("rounded", "green") or "none"."""
outline_left = BoxProperty(Color(0, 255, 0))
+ """Set the left outline of the widget e.g. ("rounded", "green") or "none"."""
keyline = KeylineProperty()
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
width = ScalarProperty(percent_unit=Unit.WIDTH)
+ """Set the width of the widget."""
height = ScalarProperty(percent_unit=Unit.HEIGHT)
+ """Set the height of the widget."""
min_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
+ """Set the minimum width of the widget."""
min_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
+ """Set the minimum height of the widget."""
max_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
+ """Set the maximum width of the widget."""
max_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
-
+ """Set the maximum height of the widget."""
dock = DockProperty()
+ """Set which edge of the parent to dock this widget to e.g. "top", "left", "right", "bottom", "none".
+ """
split = SplitProperty()
overflow_x = OverflowProperty(VALID_OVERFLOW, "hidden")
+ """Control what happens when the content extends horizontally beyond the widget's width.
+
+ Valid values are "scroll", "hidden", or "auto".
+ """
+
overflow_y = OverflowProperty(VALID_OVERFLOW, "hidden")
+ """Control what happens when the content extends vertically beyond the widget's height.
+
+ Valid values are "scroll", "hidden", or "auto".
+ """
layer = NameProperty()
layers = NameListProperty()
transitions = TransitionsProperty()
tint = ColorProperty("transparent")
+ """Set the tint of the widget. This allows you apply a opaque color above the widget.
+
+ You can specify an opacity after a color e.g. "blue 10%"
+ """
scrollbar_color = ScrollbarColorProperty("ansi_bright_magenta")
+ """Set the color of the handle of the scrollbar."""
scrollbar_color_hover = ScrollbarColorProperty("ansi_yellow")
+ """Set the color of the handle of the scrollbar when hovered."""
scrollbar_color_active = ScrollbarColorProperty("ansi_bright_yellow")
-
+ """Set the color of the handle of the scrollbar when active (being dragged)."""
scrollbar_corner_color = ScrollbarColorProperty("#666666")
-
+ """Set the color of the space between the horizontal and vertical scrollbars."""
scrollbar_background = ScrollbarColorProperty("#555555")
+ """Set the background color of the scrollbar (the track that the handle sits on)."""
scrollbar_background_hover = ScrollbarColorProperty("#444444")
+ """Set the background color of the scrollbar when hovered."""
scrollbar_background_active = ScrollbarColorProperty("black")
+ """Set the background color of the scrollbar when active (being dragged)."""
scrollbar_gutter = StringEnumProperty(
VALID_SCROLLBAR_GUTTER, "auto", layout=True, refresh_children=True
)
+ """Set to "stable" to reserve space for the scrollbar even when it's not visible.
+ This can prevent content from shifting when a scrollbar appears.
+ """
- scrollbar_size_vertical = IntegerProperty(default=1, layout=True)
+ scrollbar_size_vertical = IntegerProperty(default=2, layout=True)
+ """Set the width of the vertical scrollbar (measured in cells)."""
scrollbar_size_horizontal = IntegerProperty(default=1, layout=True)
+ """Set the height of the horizontal scrollbar (measured in cells)."""
align_horizontal = StringEnumProperty(
VALID_ALIGN_HORIZONTAL, "left", layout=True, refresh_children=True
@@ -354,6 +444,8 @@ class StylesBase:
border_subtitle_style = StyleFlagsProperty()
hatch = HatchProperty()
+ """Add a hatched background effect e.g. ("right", "yellow") or "none" to use no hatch.
+ """
overlay = StringEnumProperty(
VALID_OVERLAY, "none", layout=True, refresh_parent=True
@@ -453,22 +545,34 @@ def is_auto_height(self) -> bool:
height = self.height
return height is not None and height.unit == Unit.AUTO
- def has_rule(self, rule: str) -> bool:
+ @property
+ def is_docked(self) -> bool:
+ """Is the node docked?"""
+ dock = self.dock
+ return dock != "none"
+
+ @property
+ def is_split(self) -> bool:
+ """Is the node split?"""
+ split = self.split
+ return split != "none"
+
+ def has_rule(self, rule_name: str) -> bool:
"""Check if a rule is set on this Styles object.
Args:
- rule: Rule name.
+ rule_name: Rule name.
Returns:
``True`` if the rules is present, otherwise ``False``.
"""
raise NotImplementedError()
- def clear_rule(self, rule: str) -> bool:
+ def clear_rule(self, rule_name: str) -> bool:
"""Removes the rule from the Styles object, as if it had never been set.
Args:
- rule: Rule name.
+ rule_name: Rule name.
Returns:
``True`` if a rule was cleared, or ``False`` if the rule is already not set.
@@ -483,11 +587,11 @@ def get_rules(self) -> RulesMap:
"""
raise NotImplementedError()
- def set_rule(self, rule: str, value: object | None) -> bool:
+ def set_rule(self, rule_name: str, value: object | None) -> bool:
"""Set a rule.
Args:
- rule: Rule name.
+ rule_name: Rule name.
value: New rule value.
Returns:
@@ -495,11 +599,11 @@ def set_rule(self, rule: str, value: object | None) -> bool:
"""
raise NotImplementedError()
- def get_rule(self, rule: str, default: object = None) -> object:
+ def get_rule(self, rule_name: str, default: object = None) -> object:
"""Get an individual rule.
Args:
- rule: Name of rule.
+ rule_name: Name of rule.
default: Default if rule does not exists.
Returns:
@@ -669,9 +773,7 @@ def partial_rich_style(self) -> Style:
@dataclass
class Styles(StylesBase):
node: DOMNode | None = None
- _rules: RulesMap = field(
- default_factory=RulesMap
- ) # mypy won't be happy with `default_factory=RulesMap`
+ _rules: RulesMap = field(default_factory=RulesMap)
_updates: int = 0
important: set[str] = field(default_factory=set)
@@ -688,16 +790,16 @@ def copy(self) -> Styles:
important=self.important,
)
- def clear_rule(self, rule: str) -> bool:
+ def clear_rule(self, rule_name: str) -> bool:
"""Removes the rule from the Styles object, as if it had never been set.
Args:
- rule: Rule name.
+ rule_name: Rule name.
Returns:
``True`` if a rule was cleared, or ``False`` if it was already not set.
"""
- changed = self._rules.pop(rule, None) is not None # type: ignore
+ changed = self._rules.pop(rule_name, None) is not None # type: ignore
if changed:
self._updates += 1
return changed
@@ -1219,18 +1321,20 @@ def reset(self) -> None:
self._inline_styles.reset()
self._updates += 1
- def has_rule(self, rule: str) -> bool:
+ def has_rule(self, rule_name: str) -> bool:
"""Check if a rule has been set."""
- return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule)
+ return self._inline_styles.has_rule(rule_name) or self._base_styles.has_rule(
+ rule_name
+ )
- def set_rule(self, rule: str, value: object | None) -> bool:
+ def set_rule(self, rule_name: str, value: object | None) -> bool:
self._updates += 1
- return self._inline_styles.set_rule(rule, value)
+ return self._inline_styles.set_rule(rule_name, value)
- def get_rule(self, rule: str, default: object = None) -> object:
- if self._inline_styles.has_rule(rule):
- return self._inline_styles.get_rule(rule, default)
- return self._base_styles.get_rule(rule, default)
+ def get_rule(self, rule_name: str, default: object = None) -> object:
+ if self._inline_styles.has_rule(rule_name):
+ return self._inline_styles.get_rule(rule_name, default)
+ return self._base_styles.get_rule(rule_name, default)
def clear_rule(self, rule_name: str) -> bool:
"""Clear a rule (from inline)."""
diff --git a/src/textual/css/types.py b/src/textual/css/types.py
index ce4cebdd0b..549c21a565 100644
--- a/src/textual/css/types.py
+++ b/src/textual/css/types.py
@@ -6,8 +6,7 @@
from ..color import Color
-Edge = Literal["top", "right", "bottom", "left"]
-DockEdge = Literal["top", "right", "bottom", "left", ""]
+DockEdge = Literal["none", "top", "right", "bottom", "left"]
EdgeType = Literal[
"",
"ascii",
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 58305d0497..df629d70c4 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -2817,7 +2817,7 @@ def scroll_to_widget(
while isinstance(widget.parent, Widget) and widget is not self:
container = widget.parent
- if widget.styles.dock:
+ if widget.styles.dock != "none":
scroll_offset = Offset(0, 0)
else:
scroll_offset = container.scroll_to_region(
diff --git a/src/textual/widgets/_toast.py b/src/textual/widgets/_toast.py
index e0c7e92c7e..f9a6bf871d 100644
--- a/src/textual/widgets/_toast.py
+++ b/src/textual/widgets/_toast.py
@@ -156,7 +156,6 @@ class ToastRack(Container, inherit_css=False):
layout: vertical;
overflow-y: scroll;
margin-bottom: 1;
- margin-right: 1;
}
"""
DEFAULT_CLASSES = "-textual-system"
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_dock_none.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_dock_none.svg
new file mode 100644
index 0000000000..6171bf35a8
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_dock_none.svg
@@ -0,0 +1,79 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_notifications_loading_overlap_order.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_notifications_loading_overlap_order.svg
index 257024deb9..c4bc11885b 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_notifications_loading_overlap_order.svg
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_notifications_loading_overlap_order.svg
@@ -19,116 +19,116 @@
font-weight: 700;
}
- .terminal-3736500247-matrix {
+ .terminal-3767891991-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3736500247-title {
+ .terminal-3767891991-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3736500247-r1 { fill: #c5c8c6 }
-.terminal-3736500247-r2 { fill: #56c278 }
-.terminal-3736500247-r3 { fill: #e3e4e4 }
+ .terminal-3767891991-r1 { fill: #c5c8c6 }
+.terminal-3767891991-r2 { fill: #56c278 }
+.terminal-3767891991-r3 { fill: #e3e4e4 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- LoadingOverlayApp
+ LoadingOverlayApp
-
-
-
-
-
-
-
-
-
-▌
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌This is a big notification.
-▌
-▌
+
+
+
+
+
+
+
+
+
+▌
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌This is a big notification.
+▌
+▌
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index e68324e2bd..adba7113ca 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -11,7 +11,7 @@
from textual.containers import Vertical
from textual.pilot import Pilot
from textual.screen import Screen
-from textual.widgets import Button, Input, RichLog, TextArea, Footer
+from textual.widgets import Button, Header, Input, RichLog, TextArea, Footer
from textual.widgets import Switch
from textual.widgets import Label
from textual.widgets.text_area import BUILTIN_LANGUAGES, Selection, TextAreaTheme
@@ -863,6 +863,25 @@ def test_dock_scroll_off_by_one(snap_compare):
)
+def test_dock_none(snap_compare):
+ """Checking that `dock:none` works in CSS and Python.
+ The label should appear at the top here, since we've undocked both
+ the header and footer.
+ """
+
+ class DockNone(App[None]):
+ CSS = "Header { dock: none; }"
+
+ def compose(self) -> ComposeResult:
+ yield Label("Hello")
+ yield Header()
+ footer = Footer()
+ footer.styles.dock = "none"
+ yield footer
+
+ assert snap_compare(DockNone(), terminal_size=(30, 5))
+
+
def test_scroll_to(snap_compare):
# https://github.com/Textualize/textual/issues/2525
assert snap_compare(