Repository: Textualize/textual

Issue:
`display: none` is being ignored in vertical and horizontal layouts
Example app:

```python
from textual.app import App
from textual.widget import Widget

class ButtonsApp(App):

    def on_load(self) -> None:
        self.bind("q", "quit")

    def on_mount(self):
        self.mount(
            Widget(classes={"no-display"})
        )


ButtonsApp.run(css_file="buttons.scss", log="textual.log")
```

Example CSS:
```scss
Widget.no-display {
  display: none;
}
```

The widget is unexpectedly displayed.

Discussion:

Patch:
diff --git a/src/textual/app.py b/src/textual/app.py
index 6a6e80272..2acc04ffe 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -521,6 +521,7 @@ class App(DOMNode):
                 mount_event = events.Mount(sender=self)
                 await self.dispatch_message(mount_event)
 
+                # TODO: don't override `self.console` here
                 self.console = Console(file=sys.__stdout__)
                 self.title = self._title
                 self.refresh()
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
index 8be05f045..12a76b202 100644
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -115,12 +115,12 @@ class ScalarProperty:
         ):
             raise StyleValueError("'auto' not allowed here")
 
-        if new_value is not None and new_value.unit != Unit.AUTO:
-            if new_value.unit not in self.units:
+        if new_value.unit != Unit.AUTO:
+            if new_value is not None and new_value.unit not in self.units:
                 raise StyleValueError(
                     f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
                 )
-            if new_value.is_percent:
+            if new_value is not None and new_value.is_percent:
                 new_value = Scalar(
                     float(new_value.value), self.percent_unit, Unit.WIDTH
                 )
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
index d1ecbdd91..43c724479 100644
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -70,7 +70,7 @@ if TYPE_CHECKING:
 class RulesMap(TypedDict, total=False):
     """A typed dict for CSS rules.
 
-    Any key may be absent, indiciating that rule has not been set.
+    Any key may be absent, indicating that rule has not been set.
 
     Does not define composite rules, that is a rule that is made of a combination of other rules.
 
diff --git a/src/textual/dom.py b/src/textual/dom.py
index 7c0c0ba54..79f2cc796 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -278,6 +278,10 @@ class DOMNode(MessagePump):
         add_children(tree, self)
         return tree
 
+    @property
+    def displayed_children(self) -> list[DOMNode]:
+        return [child for child in self.children if child.display]
+
     def get_pseudo_classes(self) -> Iterable[str]:
         """Get any pseudo classes applicable to this Node, e.g. hover, focus.
 
diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py
index b507bbc67..f85e6de02 100644
--- a/src/textual/layouts/dock.py
+++ b/src/textual/layouts/dock.py
@@ -50,10 +50,9 @@ class DockLayout(Layout):
 
     def get_docks(self, parent: Widget) -> list[Dock]:
         groups: dict[str, list[Widget]] = defaultdict(list)
-        for child in parent.children:
+        for child in parent.displayed_children:
             assert isinstance(child, Widget)
-            if child.display:
-                groups[child.styles.dock].append(child)
+            groups[child.styles.dock].append(child)
         docks: list[Dock] = []
         append_dock = docks.append
         for name, edge, z in parent.styles.docks:
diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py
index 36fef9bd7..ba428c968 100644
--- a/src/textual/layouts/horizontal.py
+++ b/src/textual/layouts/horizontal.py
@@ -39,7 +39,9 @@ class HorizontalLayout(Layout):
 
         x = box_models[0].margin.left if box_models else 0
 
-        for widget, box_model, margin in zip(parent.children, box_models, margins):
+        displayed_children = parent.displayed_children
+
+        for widget, box_model, margin in zip(displayed_children, box_models, margins):
             content_width, content_height = box_model.size
             offset_y = widget.styles.align_height(content_height, parent_size.height)
             region = Region(x, offset_y, content_width, content_height)
@@ -53,4 +55,4 @@ class HorizontalLayout(Layout):
         total_region = Region(0, 0, max_width, max_height)
         add_placement(WidgetPlacement(total_region, None, 0))
 
-        return placements, set(parent.children)
+        return placements, set(displayed_children)
diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py
index 2752c4445..196f7eb52 100644
--- a/src/textual/layouts/vertical.py
+++ b/src/textual/layouts/vertical.py
@@ -40,10 +40,13 @@ class VerticalLayout(Layout):
 
         y = box_models[0].margin.top if box_models else 0
 
-        for widget, box_model, margin in zip(parent.children, box_models, margins):
+        displayed_children = parent.displayed_children
+
+        for widget, box_model, margin in zip(displayed_children, box_models, margins):
             content_width, content_height = box_model.size
             offset_x = widget.styles.align_width(content_width, parent_size.width)
             region = Region(offset_x, y, content_width, content_height)
+            # TODO: it seems that `max_height` is not used?
             max_height = max(max_height, content_height)
             add_placement(WidgetPlacement(region, widget, 0))
             y += region.height + margin
@@ -54,4 +57,4 @@ class VerticalLayout(Layout):
         total_region = Region(0, 0, max_width, max_height)
         add_placement(WidgetPlacement(total_region, None, 0))
 
-        return placements, set(parent.children)
+        return placements, set(displayed_children)


Test patch:
diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py
index 49e4545ad..039d5745f 100644
--- a/tests/css/test_styles.py
+++ b/tests/css/test_styles.py
@@ -1,15 +1,11 @@
-from decimal import Decimal
-
 import pytest
 
 from rich.style import Style
 
 from textual.color import Color
-from textual.css.errors import StyleTypeError, StyleValueError
-from textual.css.scalar import Scalar, Unit
+from textual.css.errors import StyleTypeError
 from textual.css.styles import Styles, RenderStyles
 from textual.dom import DOMNode
-from textual.widget import Widget
 
 
 def test_styles_reset():
@@ -135,53 +131,3 @@ def test_opacity_set_invalid_type_error():
     styles = RenderStyles(DOMNode(), Styles(), Styles())
     with pytest.raises(StyleTypeError):
         styles.opacity = "invalid value"
-
-
-@pytest.mark.parametrize(
-    "size_dimension_input,size_dimension_expected_output",
-    [
-        # fmt: off
-        [None, None],
-        [1, Scalar(1, Unit.CELLS, Unit.WIDTH)],
-        [1.0, Scalar(1.0, Unit.CELLS, Unit.WIDTH)],
-        [1.2, Scalar(1.2, Unit.CELLS, Unit.WIDTH)],
-        [1.2e3, Scalar(1200.0, Unit.CELLS, Unit.WIDTH)],
-        ["20", Scalar(20, Unit.CELLS, Unit.WIDTH)],
-        ["1.4", Scalar(1.4, Unit.CELLS, Unit.WIDTH)],
-        [Scalar(100, Unit.CELLS, Unit.WIDTH), Scalar(100, Unit.CELLS, Unit.WIDTH)],
-        [Scalar(10.3, Unit.CELLS, Unit.WIDTH), Scalar(10.3, Unit.CELLS, Unit.WIDTH)],
-        [Scalar(10.4, Unit.CELLS, Unit.HEIGHT), Scalar(10.4, Unit.CELLS, Unit.HEIGHT)],
-        [Scalar(10.5, Unit.PERCENT, Unit.WIDTH), Scalar(10.5, Unit.WIDTH, Unit.WIDTH)],
-        [Scalar(10.6, Unit.PERCENT, Unit.PERCENT), Scalar(10.6, Unit.WIDTH, Unit.WIDTH)],
-        [Scalar(10.7, Unit.HEIGHT, Unit.PERCENT), Scalar(10.7, Unit.HEIGHT, Unit.PERCENT)],
-        # percentage values are normalised to floats and get the WIDTH "percent_unit":
-        [Scalar(11, Unit.PERCENT, Unit.HEIGHT), Scalar(11.0, Unit.WIDTH, Unit.WIDTH)],
-        # fmt: on
-    ],
-)
-def test_widget_style_size_can_accept_various_data_types_and_normalize_them(
-    size_dimension_input, size_dimension_expected_output
-):
-    widget = Widget()
-
-    widget.styles.width = size_dimension_input
-    assert widget.styles.width == size_dimension_expected_output
-
-
-@pytest.mark.parametrize(
-    "size_dimension_input",
-    [
-        "a",
-        "1.4e3",
-        3.14j,
-        Decimal("3.14"),
-        list(),
-        tuple(),
-        dict(),
-    ],
-)
-def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_input):
-    widget = Widget()
-
-    with pytest.raises(StyleValueError):
-        widget.styles.width = size_dimension_input
diff --git a/tests/layouts/test_common_layout_features.py b/tests/layouts/test_common_layout_features.py
new file mode 100644
index 000000000..e8cc5ee19
--- /dev/null
+++ b/tests/layouts/test_common_layout_features.py
@@ -0,0 +1,30 @@
+import pytest
+
+from textual.screen import Screen
+from textual.widget import Widget
+
+
+@pytest.mark.parametrize(
+    "layout,display,expected_in_displayed_children",
+    [
+        ("dock", "block", True),
+        ("horizontal", "block", True),
+        ("vertical", "block", True),
+        ("dock", "none", False),
+        ("horizontal", "none", False),
+        ("vertical", "none", False),
+    ],
+)
+def test_nodes_take_display_property_into_account_when_they_display_their_children(
+    layout: str, display: str, expected_in_displayed_children: bool
+):
+    widget = Widget(name="widget that might not be visible 🥷 ")
+    widget.styles.display = display
+
+    screen = Screen()
+    screen.styles.layout = layout
+    screen.add_child(widget)
+
+    displayed_children = screen.displayed_children
+    assert isinstance(displayed_children, list)
+    assert (widget in screen.displayed_children) is expected_in_displayed_children
diff --git a/tests/test_widget.py b/tests/test_widget.py
index abc92039c..7eee43a54 100644
--- a/tests/test_widget.py
+++ b/tests/test_widget.py
@@ -1,22 +1,16 @@
-from contextlib import nullcontext as does_not_raise
-from decimal import Decimal
-
 import pytest
 
 from textual.css.errors import StyleValueError
-from textual.css.scalar import Scalar, Unit
 from textual.widget import Widget
 
 
 @pytest.mark.parametrize(
-    "set_val, get_val, style_str",
-    [
+    "set_val, get_val, style_str", [
         [True, True, "visible"],
         [False, False, "hidden"],
         ["hidden", False, "hidden"],
         ["visible", True, "visible"],
-    ],
-)
+    ])
 def test_widget_set_visible_true(set_val, get_val, style_str):
     widget = Widget()
     widget.visible = set_val