From a09773f436b3eff793a693922c528faa8151d477 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?=
<5621605+rodrigogiraoserrao@users.noreply.github.com>
Date: Thu, 31 Aug 2023 14:03:44 +0100
Subject: [PATCH 01/14] DataTable new rows can have auto height.
Related issue: #3122.
---
src/textual/widgets/_data_table.py | 45 ++++++++++++++++++++++++++----
1 file changed, 39 insertions(+), 6 deletions(-)
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index 6e7e3e543f..950467937d 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -187,6 +187,7 @@ class Row:
key: RowKey
height: int
label: Text | None = None
+ auto_height: bool = False
class RowRenderables(NamedTuple):
@@ -1190,8 +1191,16 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
self._require_update_dimensions = True
def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
- """Called to recalculate the virtual (scrollable) size."""
+ """Called to recalculate the virtual (scrollable) size.
+
+ This recomputes column widths and then checks if any of the new rows need
+ to have their height computed.
+
+ Args:
+ new_rows: The new rows that will affect the `DataTable` dimensions.
+ """
console = self.app.console
+ auto_height_rows: list[tuple[Row, list[RenderableType]]] = []
for row_key in new_rows:
row_index = self._row_locations.get(row_key)
@@ -1201,6 +1210,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
continue
row = self.rows.get(row_key)
+ assert row is not None
if row.label is not None:
self._labelled_row_exists = True
@@ -1215,6 +1225,22 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
content_width = measure(console, renderable, 1)
column.content_width = max(column.content_width, content_width)
+ if row.auto_height:
+ auto_height_rows.append((row, cells_in_row))
+
+ for row, cells_in_row in auto_height_rows:
+ height = 0
+ for column, renderable in zip(self.ordered_columns, cells_in_row):
+ height = max(
+ height,
+ len(
+ console.render_lines(
+ renderable, console.options.update_width(column.width)
+ )
+ ),
+ )
+ row.height = height
+
self._clear_caches()
data_cells_width = sum(column.render_width for column in self.columns.values())
@@ -1373,7 +1399,7 @@ def add_column(
def add_row(
self,
*cells: CellType,
- height: int = 1,
+ height: int | None = 1,
key: str | None = None,
label: TextType | None = None,
) -> RowKey:
@@ -1381,13 +1407,14 @@ def add_row(
Args:
*cells: Positional arguments should contain cell data.
- height: The height of a row (in lines).
+ height: The height of a row (in lines). Use `None` to auto-detect the optimal
+ height.
key: A key which uniquely identifies this row. If None, it will be generated
for you and returned.
label: The label for the row. Will be displayed to the left if supplied.
Returns:
- Uniquely identifies this row. Can be used to retrieve this row regardless
+ Unique identifier for this row. Can be used to retrieve this row regardless
of its current location in the DataTable (it could have moved after
being added due to sorting or insertion/deletion of other rows).
"""
@@ -1407,7 +1434,12 @@ def add_row(
for column, cell in zip_longest(self.ordered_columns, cells)
}
label = Text.from_markup(label) if isinstance(label, str) else label
- self.rows[row_key] = Row(row_key, height, label)
+ self.rows[row_key] = Row(
+ row_key,
+ height if height is not None else 1,
+ label,
+ height is None,
+ )
self._new_rows.add(row_key)
self._require_update_dimensions = True
self.cursor_coordinate = self.cursor_coordinate
@@ -1546,7 +1578,8 @@ async def _on_idle(self, _: events.Idle) -> None:
if self._require_update_dimensions:
# Add the new rows *before* updating the column widths, since
- # cells in a new row may influence the final width of a column
+ # cells in a new row may influence the final width of a column.
+ # Only then can we compute optimal height of rows with "auto" height.
self._require_update_dimensions = False
new_rows = self._new_rows.copy()
self._new_rows.clear()
From c3904e5c693cab73d759c6dbb1323d1a513aa3fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?=
<5621605+rodrigogiraoserrao@users.noreply.github.com>
Date: Thu, 31 Aug 2023 14:05:35 +0100
Subject: [PATCH 02/14] Test auto height computation in DataTable.add_row
---
CHANGELOG.md | 1 +
tests/test_data_table.py | 60 +++++++++++++++++++++++++++-------------
2 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3c5ce003f..8939ba7d70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Reactive callbacks are now scheduled on the message pump of the reactable that is watching instead of the owner of reactive attribute https://github.com/Textualize/textual/pull/3065
- Callbacks scheduled with `call_next` will now have the same prevented messages as when the callback was scheduled https://github.com/Textualize/textual/pull/3065
- Added `cursor_type` to the `DataTable` constructor.
+- `DataTable.add_row` accepts `height=None` to automatically compute optimal height for a row https://github.com/Textualize/textual/pull/3213
### Fixed
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
index e00b9432a4..763f624a27 100644
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import pytest
+from rich.panel import Panel
from rich.text import Text
from textual._wait import wait_for_idle
@@ -419,11 +420,11 @@ async def test_get_cell_coordinate_returns_coordinate():
table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
table.add_row("ValR3C1", "ValR3C2", "ValR3C3", key="R3")
- assert table.get_cell_coordinate('R1', 'C1') == Coordinate(0, 0)
- assert table.get_cell_coordinate('R2', 'C2') == Coordinate(1, 1)
- assert table.get_cell_coordinate('R1', 'C3') == Coordinate(0, 2)
- assert table.get_cell_coordinate('R3', 'C1') == Coordinate(2, 0)
- assert table.get_cell_coordinate('R3', 'C2') == Coordinate(2, 1)
+ assert table.get_cell_coordinate("R1", "C1") == Coordinate(0, 0)
+ assert table.get_cell_coordinate("R2", "C2") == Coordinate(1, 1)
+ assert table.get_cell_coordinate("R1", "C3") == Coordinate(0, 2)
+ assert table.get_cell_coordinate("R3", "C1") == Coordinate(2, 0)
+ assert table.get_cell_coordinate("R3", "C2") == Coordinate(2, 1)
async def test_get_cell_coordinate_invalid_row_key():
@@ -434,7 +435,7 @@ async def test_get_cell_coordinate_invalid_row_key():
table.add_row("TargetValue", key="R1")
with pytest.raises(CellDoesNotExist):
- coordinate = table.get_cell_coordinate('INVALID_ROW', 'C1')
+ coordinate = table.get_cell_coordinate("INVALID_ROW", "C1")
async def test_get_cell_coordinate_invalid_column_key():
@@ -445,7 +446,7 @@ async def test_get_cell_coordinate_invalid_column_key():
table.add_row("TargetValue", key="R1")
with pytest.raises(CellDoesNotExist):
- coordinate = table.get_cell_coordinate('R1', 'INVALID_COLUMN')
+ coordinate = table.get_cell_coordinate("R1", "INVALID_COLUMN")
async def test_get_cell_at_returns_value_at_cell():
@@ -531,9 +532,9 @@ async def test_get_row_index_returns_index():
table.add_row("ValR2C1", "ValR2C2", key="R2")
table.add_row("ValR3C1", "ValR3C2", key="R3")
- assert table.get_row_index('R1') == 0
- assert table.get_row_index('R2') == 1
- assert table.get_row_index('R3') == 2
+ assert table.get_row_index("R1") == 0
+ assert table.get_row_index("R2") == 1
+ assert table.get_row_index("R3") == 2
async def test_get_row_index_invalid_row_key():
@@ -544,7 +545,7 @@ async def test_get_row_index_invalid_row_key():
table.add_row("TargetValue", key="R1")
with pytest.raises(RowDoesNotExist):
- index = table.get_row_index('InvalidRow')
+ index = table.get_row_index("InvalidRow")
async def test_get_column():
@@ -591,6 +592,7 @@ async def test_get_column_at_invalid_index(index):
with pytest.raises(ColumnDoesNotExist):
list(table.get_column_at(index))
+
async def test_get_column_index_returns_index():
app = DataTableApp()
async with app.run_test():
@@ -598,12 +600,12 @@ async def test_get_column_index_returns_index():
table.add_column("Column1", key="C1")
table.add_column("Column2", key="C2")
table.add_column("Column3", key="C3")
- table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1")
- table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
+ table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1")
+ table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
- assert table.get_column_index('C1') == 0
- assert table.get_column_index('C2') == 1
- assert table.get_column_index('C3') == 2
+ assert table.get_column_index("C1") == 0
+ assert table.get_column_index("C2") == 1
+ assert table.get_column_index("C3") == 2
async def test_get_column_index_invalid_column_key():
@@ -613,11 +615,10 @@ async def test_get_column_index_invalid_column_key():
table.add_column("Column1", key="C1")
table.add_column("Column2", key="C2")
table.add_column("Column3", key="C3")
- table.add_row("TargetValue1", "TargetValue2", "TargetValue3", key="R1")
+ table.add_row("TargetValue1", "TargetValue2", "TargetValue3", key="R1")
with pytest.raises(ColumnDoesNotExist):
- index = table.get_column_index('InvalidCol')
-
+ index = table.get_column_index("InvalidCol")
async def test_update_cell_cell_exists():
@@ -1161,3 +1162,24 @@ async def test_unset_hover_highlight_when_no_table_cell_under_mouse():
# the widget, and the hover cursor is hidden
await pilot.hover(DataTable, offset=Offset(42, 1))
assert not table._show_hover_cursor
+
+
+@pytest.mark.parametrize(
+ ["cell", "height"],
+ [
+ ("hey there", 1),
+ (Text("hey there"), 1),
+ (Text("long string", overflow="fold"), 2),
+ (Panel.fit("Hello\nworld"), 4),
+ ("1\n2\n3\n4\n5\n6\n7", 7),
+ ],
+)
+async def test_add_row_auto_height(cell: RenderableType, height: int):
+ app = DataTableApp()
+ async with app.run_test() as pilot:
+ table = app.query_one(DataTable)
+ table.add_column("C", width=10)
+ row_key = table.add_row(cell, height=None)
+ row = table.rows.get(row_key)
+ await pilot.pause()
+ assert row.height == height
From 65aaf397a5aeab53e183c6ac567271825115ab6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?=
<5621605+rodrigogiraoserrao@users.noreply.github.com>
Date: Thu, 31 Aug 2023 16:10:15 +0100
Subject: [PATCH 03/14] Add snapshot test for add_row height=None.
---
.../__snapshots__/test_snapshots.ambr | 158 ++++++++++++++++++
.../data_table_add_row_auto_height.py | 21 +++
tests/snapshot_tests/test_snapshots.py | 5 +
3 files changed, 184 insertions(+)
create mode 100644 tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index bd2ffa863b..4e25c90d5d 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -12731,6 +12731,164 @@
'''
# ---
+# name: test_datatable_add_row_auto_height
+ '''
+
+
+ '''
+# ---
# name: test_datatable_column_cursor_render
'''
+
+ '''
+# ---
+# name: test_datatable_add_row_auto_height_sorted
+ '''
+