Repository: Textualize/textual

Issue:
Grid columns with multiple units resolves percent unit to wrong value
In a grid layout with mixed units in the style `grid-columns`, the column associated with `25%` gets the wrong size.
However, using `25w` or `25vw` instead of `25%` produces the correct result.

Here is the screenshot of the app where the middle column should take up 25% of the total width and the 1st and 4th columns should accommodate that:

![Screenshot at Dec 19 17-30-02](https://user-images.githubusercontent.com/5621605/208485031-2864c964-b7c6-496b-a173-8618f6a3ba70.png)


<details>
<summary><code>grid_columns.py</code></summary>

```py
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label


class MyApp(App):
    def compose(self):
        yield Grid(
            Label("1fr"),
            Label("width = 16"),
            Label("middle"),
            Label("1fr"),
            Label("width = 16"),
        )


app = MyApp(css_path="grid_columns.css")
```
</details>

<details>
<summary><code>grid_columns.css</code></summary>

```css
Grid {
    grid-size: 5 1;
    grid-columns: 1fr 16 25%;
}

Label {
    border: round white;
    content-align-horizontal: center;
    width: 100%;
    height: 100%;
}
```

</details>

Try running the app with `textual run --dev grid_columns.css` and change the value `25%` to `25w` or `25vw` and notice it gets set to the correct value.


Discussion:
A similar application containing a `Horizontal` layout and labels shows this might be a bug with `grid-columns` or the way grid columns are computed and not with the way units are resolved (the app below shows the labels correctly sized):

```py
# relative_sizes.py
from textual.app import App
from textual.containers import Horizontal
from textual.widgets import Label


class MyApp(App):
    def compose(self):
        yield Horizontal(
            Label("1fr", classes="fr"),
            Label("width = 16", classes="fixed"),
            Label("middle", classes="percent"),
            Label("1fr", classes="fr"),
            Label("width = 16", classes="fixed"),
        )


app = MyApp(css_path="relative_sizes.css")
```

```sass
/* relative_sizes.css */
Label {
    border: round white;
    content-align-horizontal: center;
    height: 100%;
}

Label.fr {
    width: 1fr;
}

Label.fixed {
    width: 16;
}

Label.percent {
    width: 25%;
}
```
Printing the values of `row_scalars` and `column_scalars` at the very beginning of `GridLayout.arrange` in

https://github.com/Textualize/textual/blob/cc1f2f61d9cc39bf9becbe53f8bff5634575c93c/src/textual/layouts/grid.py#L25

shows that the unit `%` is relative to the terminal _height_ instead of the terminal width, which is why the column isn't as wide as it should be:

```
[10:28:32] PRINT                                                      grid.py:27
(Scalar(value=1.0, unit=<Unit.FRACTION: 2>, percent_unit=<Unit.HEIGHT: 5>),
Scalar(value=16.0, unit=<Unit.CELLS: 1>, percent_unit=<Unit.WIDTH: 4>),
Scalar(value=25.0, unit=<Unit.PERCENT: 3>, percent_unit=<Unit.HEIGHT: 5>))
```

The screen recording below proves this, as making the terminal taller makes the middle column wider:

https://user-images.githubusercontent.com/5621605/214018483-b6962234-c277-479e-bee8-e96cd6c15ab5.mov


Resolving patch:
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -202,6 +202,9 @@ def __set__(
 
 
 class ScalarListProperty:
+    def __init__(self, percent_unit: Unit) -> None:
+        self.percent_unit = percent_unit
+
     def __set_name__(self, owner: Styles, name: str) -> None:
         self.name = name
 
@@ -229,7 +232,7 @@ def __set__(
                 scalars.append(Scalar.from_number(parse_value))
             else:
                 scalars.append(
-                    Scalar.parse(parse_value)
+                    Scalar.parse(parse_value, self.percent_unit)
                     if isinstance(parse_value, str)
                     else parse_value
                 )
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -865,16 +865,12 @@ def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> N
 
     def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None:
         scalars: list[Scalar] = []
+        percent_unit = Unit.WIDTH if name == "grid-columns" else Unit.HEIGHT
         for token in tokens:
             if token.name == "number":
                 scalars.append(Scalar.from_number(float(token.value)))
             elif token.name == "scalar":
-                scalars.append(
-                    Scalar.parse(
-                        token.value,
-                        percent_unit=Unit.WIDTH if name == "rows" else Unit.HEIGHT,
-                    )
-                )
+                scalars.append(Scalar.parse(token.value, percent_unit=percent_unit))
             else:
                 self.error(
                     name,
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -277,8 +277,8 @@ class StylesBase(ABC):
     content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
     content_align = AlignProperty()
 
-    grid_rows = ScalarListProperty()
-    grid_columns = ScalarListProperty()
+    grid_rows = ScalarListProperty(percent_unit=Unit.HEIGHT)
+    grid_columns = ScalarListProperty(percent_unit=Unit.WIDTH)
 
     grid_size_columns = IntegerProperty(default=1, layout=True)
     grid_size_rows = IntegerProperty(default=0, layout=True)


Test patch:
diff --git a/tests/css/test_grid_rows_columns_relative_units.py b/tests/css/test_grid_rows_columns_relative_units.py
new file mode 100644
--- /dev/null
+++ b/tests/css/test_grid_rows_columns_relative_units.py
@@ -0,0 +1,42 @@
+"""
+Regression tests for #1406 https://github.com/Textualize/textual/issues/1406
+
+The scalars in grid-rows and grid-columns were not aware of the dimension
+they should be relative to.
+"""
+
+from textual.css.parse import parse_declarations
+from textual.css.scalar import Unit
+from textual.css.styles import Styles
+
+
+def test_grid_rows_columns_relative_units_are_correct():
+    """Ensure correct relative dimensions for programmatic assignments."""
+
+    styles = Styles()
+
+    styles.grid_columns = "1fr 5%"
+    fr, percent = styles.grid_columns
+    assert fr.percent_unit == Unit.WIDTH
+    assert percent.percent_unit == Unit.WIDTH
+
+    styles.grid_rows = "1fr 5%"
+    fr, percent = styles.grid_rows
+    assert fr.percent_unit == Unit.HEIGHT
+    assert percent.percent_unit == Unit.HEIGHT
+
+
+def test_styles_builder_uses_correct_relative_units_grid_rows_columns():
+    """Ensure correct relative dimensions for CSS parsed from files."""
+
+    CSS = "grid-rows: 1fr 5%; grid-columns: 1fr 5%;"
+
+    styles = parse_declarations(CSS, "test")
+
+    fr, percent = styles.grid_columns
+    assert fr.percent_unit == Unit.WIDTH
+    assert percent.percent_unit == Unit.WIDTH
+
+    fr, percent = styles.grid_rows
+    assert fr.percent_unit == Unit.HEIGHT
+    assert percent.percent_unit == Unit.HEIGHT