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 @@ + + + + + + + + + + + + + + + + + + + + + + + DockNone + + + + + + + + + + Hello                          +DockNone +^p palette + + + + + 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(