From cb48fa9210109adde2151fbe0816c9e9deaa2d35 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 29 Dec 2024 10:05:09 +0000 Subject: [PATCH] fix grid sizing --- src/textual/layouts/grid.py | 4 + src/textual/widget.py | 7 +- .../test_snapshots/test_widgets_in_grid.svg | 255 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 37 +++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots/test_widgets_in_grid.svg diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 6b4304092b..63eb291ad5 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -253,6 +253,7 @@ def apply_height_limits(widget: Widget, height: int) -> int: + gutter_height, ) height = max(height, widget_height) + row_scalars[row] = Scalar.from_number(height) rows = resolve(row_scalars, size.height, gutter_horizontal, size, viewport) @@ -271,12 +272,15 @@ def apply_height_limits(widget: Widget, height: int) -> int: x2, cell_width = columns[min(max_column, column + column_span)] y2, cell_height = rows[min(max_row, row + row_span)] cell_size = Size(cell_width + x2 - x, cell_height + y2 - y) + box_width, box_height, margin = widget._get_box_model( cell_size, viewport, Fraction(cell_size.width), Fraction(cell_size.height), + constrain_width=True, ) + if self.stretch_height and len(children) > 1: if box_height <= cell_size.height: box_height = Fraction(cell_size.height) diff --git a/src/textual/widget.py b/src/textual/widget.py index 5112a132d0..d71d311715 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1455,14 +1455,16 @@ def _get_box_model( viewport: Size, width_fraction: Fraction, height_fraction: Fraction, + constrain_width: bool = False, ) -> BoxModel: """Process the box model for this widget. Args: - container: The size of the container widget (with a layout) + container: The size of the container widget (with a layout). viewport: The viewport size. width_fraction: A fraction used for 1 `fr` unit on the width dimension. height_fraction: A fraction used for 1 `fr` unit on the height dimension. + constrain_width: Restrict the width to the container width. Returns: The size and margin for this widget. @@ -1534,6 +1536,9 @@ def _get_box_model( content_width = max(Fraction(0), content_width) + if constrain_width: + content_width = min(Fraction(container.width - gutter.width), content_width) + if styles.height is None: # No height specified, fill the available space content_height = Fraction(content_container.height - margin.height) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_widgets_in_grid.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_widgets_in_grid.svg new file mode 100644 index 0000000000..bb8cea6522 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_widgets_in_grid.svg @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ┏━ 0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 2 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +I must not fear.               ┃┃I must not fear.               ┃┃I must not fear.                 +Fear is the mind-killer.       ┃┃Fear is the mind-killer.       ┃┃Fear is the mind-killer.         +Fear is the little-death that  ┃┃Fear is the little-death that  ┃┃Fear is the little-death that    +brings total obliteration.     ┃┃brings total obliteration.     ┃┃brings total obliteration.       +I will face my fear.           ┃┃I will face my fear.           ┃┃I will face my fear.             +I will permit it to pass over  ┃┃I will permit it to pass over  ┃┃I will permit it to pass over me +me and through me.             ┃┃me and through me.             ┃┃and through me.                  +And when it has gone past, I   ┃┃And when it has gone past, I   ┃┃And when it has gone past, I     +will turn the inner eye to see ┃┃will turn the inner eye to see ┃┃will turn the inner eye to see   +its path.                      ┃┃its path.                      ┃┃its path.                        +Where the fear has gone there  ┃┃Where the fear has gone there  ┃┃Where the fear has gone there    +will be nothing. Only I will   ┃┃will be nothing. Only I will   ┃┃will be nothing. Only I will     +remain.                        ┃┃remain.                        ┃┃remain.                          +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━ 3 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 4 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 5 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +I must not fear.               ┃┃I must not fear.               ┃┃I must not fear.                 +Fear is the mind-killer.       ┃┃Fear is the mind-killer.       ┃┃Fear is the mind-killer.         +Fear is the little-death that  ┃┃Fear is the little-death that  ┃┃Fear is the little-death that    +brings total obliteration.     ┃┃brings total obliteration.     ┃┃brings total obliteration.       +I will face my fear.           ┃┃I will face my fear.           ┃┃I will face my fear.             +I will permit it to pass over  ┃┃I will permit it to pass over  ┃┃I will permit it to pass over me +me and through me.             ┃┃me and through me.             ┃┃and through me.                  +And when it has gone past, I   ┃┃And when it has gone past, I   ┃┃And when it has gone past, I     +will turn the inner eye to see ┃┃will turn the inner eye to see ┃┃will turn the inner eye to see   +its path.                      ┃┃its path.                      ┃┃its path.                        +Where the fear has gone there  ┃┃Where the fear has gone there  ┃┃Where the fear has gone there    +will be nothing. Only I will   ┃┃will be nothing. Only I will   ┃┃will be nothing. Only I will     +remain.                        ┃┃remain.                        ┃┃remain.                          +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━ 6 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 7 ━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┏━ 8 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +I must not fear.               ┃┃I must not fear.               ┃┃I must not fear.                 +Fear is the mind-killer.       ┃┃Fear is the mind-killer.       ┃┃Fear is the mind-killer.         +Fear is the little-death that  ┃┃Fear is the little-death that  ┃┃Fear is the little-death that    +brings total obliteration.     ┃┃brings total obliteration.     ┃┃brings total obliteration.       +I will face my fear.           ┃┃I will face my fear.           ┃┃I will face my fear.             +I will permit it to pass over  ┃┃I will permit it to pass over  ┃┃I will permit it to pass over me +me and through me.             ┃┃me and through me.             ┃┃and through me.                  +And when it has gone past, I   ┃┃And when it has gone past, I   ┃┃And when it has gone past, I     +will turn the inner eye to see ┃┃will turn the inner eye to see ┃┃will turn the inner eye to see   +its path.                      ┃┃its path.                      ┃┃its path.                        +Where the fear has gone there  ┃┃Where the fear has gone there  ┃┃Where the fear has gone there    +will be nothing. Only I will   ┃┃will be nothing. Only I will   ┃┃will be nothing. Only I will     +remain.                        ┃┃remain.                        ┃┃remain.                          +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 1daba6b805..54c2e9a9ae 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -17,6 +17,7 @@ Grid, Middle, Vertical, + VerticalGroup, VerticalScroll, HorizontalGroup, ) @@ -3193,3 +3194,39 @@ def compose(self): yield MyListView() snap_compare(TUI(), press=["down", "enter", "down", "down", "enter"]) + + +def test_widgets_in_grid(snap_compare): + """You should see a 3x3 grid of labels where the text is wrapped, and there is no superfluous space.""" + TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain.""" + + class MyApp(App): + CSS = """ + VerticalGroup { + layout: grid; + grid-size: 3 3; + grid-columns: 1fr; + grid-rows: auto; + height: auto; + background: blue; + } + Label { + border: heavy red; + text-align: left; + } + """ + + def compose(self) -> ComposeResult: + with VerticalGroup(): + for n in range(9): + label = Label(TEXT, id=f"label{n}") + label.border_title = str(n) + yield label + + snap_compare(MyApp(), terminal_size=(100, 50))