From ca20d023d53ee8296fd2a641bbfec99cfaafff27 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 29 Jan 2026 14:14:15 +0000 Subject: [PATCH 1/8] extend table --- src/textual/widgets/_data_table.py | 39 +++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 04e9b0599e..70a8bea96a 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -1555,7 +1555,7 @@ def _get_row_region(self, row_index: int) -> Region: y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) if self.show_header: y += self.header_height - row_region = Region(0, y, row_width, row.height) + row_region = Region(0, y, max(self.size.width, row_width), row.height) return row_region def _get_column_region(self, column_index: int) -> Region: @@ -2357,17 +2357,34 @@ def _render_line_in_row( ) remaining_space = max(0, widget_width - table_width) background_color = self.background_colors[1] - if row_style.bgcolor is not None: - # TODO: This should really be in a component class - faded_color = Color.from_rich_color(row_style.bgcolor).blend( - background_color, factor=0.25 - ) - faded_style = Style.from_color( - color=row_style.color, bgcolor=faded_color.rich_color + if self.cursor_type == "row": + extend_style, _ = self._get_styles_to_render_cell( + row_index == -1, + False, + False, + should_highlight( + hover_location, Coordinate(row_index or 0, 0), cursor_type + ), + row_index == cursor_location.row, + self.show_cursor, + self._show_hover_cursor, + False, + False, ) + extend_style = row_style + extend_style else: - faded_style = Style.from_color(row_style.color, row_style.bgcolor) - scrollable_row.append([Segment(" " * remaining_space, faded_style)]) + if row_style.bgcolor is not None: + # TODO: This should really be in a component class + faded_color = Color.from_rich_color(row_style.bgcolor).blend( + background_color, factor=0.25 + ) + extend_style = Style.from_color( + color=row_style.color, bgcolor=faded_color.rich_color + ) + else: + extend_style = Style.from_color(row_style.color, row_style.bgcolor) + extend_style += Style.from_meta({"row": row_index, "column": 0}) + scrollable_row.append([Segment(" " * remaining_space, extend_style)]) row_pair = (fixed_row, scrollable_row) self._row_render_cache[cache_key] = row_pair @@ -2389,7 +2406,7 @@ def _get_offsets(self, y: int) -> tuple[RowKey, int]: return self._header_row_key, y y -= header_height if y > len(y_offsets): - raise LookupError("Y coord {y!r} is greater than total height") + raise LookupError(f"Y coord {y!r} is greater than total height") return y_offsets[y] From 5b4ac03b36c80e4c8586a333da2566f81ff853a7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 29 Jan 2026 17:23:08 +0000 Subject: [PATCH 2/8] fix tests --- CHANGELOG.md | 7 +++++++ src/textual/widgets/_data_table.py | 14 +++++++++----- tests/test_data_table.py | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e1bd2dd9..649032a770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Changed + +- The DataTable row cursor will extend to the full width if there is excess space +- The DataTable will send a selected event on click, only if the cell / row / column is currently highlighted + ## [7.4.0] - 2026-01-25 ### Added diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 70a8bea96a..8c60d900e0 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -268,6 +268,8 @@ class RowRenderables(NamedTuple): class DataTable(ScrollView, Generic[CellType], can_focus=True): """A tabular widget that contains data.""" + ALLOW_SELECT = False + BINDINGS: ClassVar[list[BindingType]] = [ Binding("enter", "select_cursor", "Select", show=False), Binding("up", "cursor_up", "Cursor up", show=False), @@ -327,8 +329,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): background: $surface; color: $foreground; height: auto; - max-height: 100%; - + max-height: 100%; &.datatable--fixed-cursor { background: $block-cursor-blurred-background; } @@ -450,7 +451,7 @@ def __init__( ) -> None: self.data_table = data_table """The data table.""" - self.value: CellType = value + self.value = value """The value in the highlighted cell.""" self.coordinate: Coordinate = coordinate """The coordinate of the highlighted cell.""" @@ -2684,8 +2685,11 @@ async def _on_click(self, event: events.Click) -> None: self.post_message(message) elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. - self.cursor_coordinate = Coordinate(row_index, column_index) - self._post_selected_message() + new_coordinate = Coordinate(row_index, column_index) + highlight_click = new_coordinate == self.cursor_coordinate + self.cursor_coordinate = new_coordinate + if highlight_click: + self._post_selected_message() self._scroll_cursor_into_view(animate=True) event.stop() diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 63032c67b0..f23373def7 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -29,6 +29,7 @@ class DataTableApp(App): + AUTO_FOCUS = None messages_to_record = { "CellHighlighted", "CellSelected", @@ -797,6 +798,7 @@ async def test_datatable_click_cell_cursor(): table.add_row("123") row_key = table.add_row("456") await pilot.click(offset=Offset(1, 2)) + await pilot.click(offset=Offset(1, 2)) # There's two CellHighlighted events since a cell is highlighted on initial load, # then when we click, another cell is highlighted (and selected). assert app.message_names == [ @@ -825,15 +827,22 @@ async def test_click_row_cursor(): table.add_column("ABC") table.add_row("123") row_key = table.add_row("456") + await pilot.click(offset=Offset(1, 2)) - assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"] + print(repr(app.message_names)) + assert app.message_names == ["RowHighlighted", "RowHighlighted"] row_highlighted: DataTable.RowHighlighted = app.messages[1] assert row_highlighted.row_key == row_key + assert row_highlighted.cursor_row == 1 - row_selected: DataTable.RowSelected = app.messages[2] + app.messages.clear() + await pilot.click(offset=Offset(1, 2)) + assert app.message_names == ["RowSelected"] + + row_selected: DataTable.RowSelected = app.messages[0] assert row_selected.row_key == row_key assert row_highlighted.cursor_row == 1 @@ -849,6 +858,7 @@ async def test_click_column_cursor(): table.add_row("123") table.add_row("456") await pilot.click(offset=Offset(1, 2)) + await pilot.click(offset=Offset(1, 2)) assert app.message_names == [ "ColumnHighlighted", "ColumnHighlighted", From cc4ebf6ea3afab3b8eeae5edb94aa0d7ff771064 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 29 Jan 2026 17:30:56 +0000 Subject: [PATCH 3/8] snapshitS --- src/textual/widgets/_data_table.py | 6 +- .../test_datatable_hot_reloading.svg | 114 +++++++++--------- .../test_datatable_row_cursor_render.svg | 114 +++++++++--------- 3 files changed, 118 insertions(+), 116 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 8c60d900e0..065132fdce 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -2384,7 +2384,9 @@ def _render_line_in_row( ) else: extend_style = Style.from_color(row_style.color, row_style.bgcolor) - extend_style += Style.from_meta({"row": row_index, "column": 0}) + extend_style += Style.from_meta( + {"row": row_index, "column": 0, "out_of_bounds": True} + ) scrollable_row.append([Segment(" " * remaining_space, extend_style)]) row_pair = (fixed_row, scrollable_row) @@ -2555,7 +2557,7 @@ def _on_mouse_move(self, event: events.MouseMove): and column metadata from the segments present in the cells.""" self._set_hover_cursor(True) meta = event.style.meta - if not meta: + if not meta or meta.get("out_of_bounds"): self._set_hover_cursor(False) return diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg index 77272ea020..24e5fcd323 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-1293670551-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1293670551-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1293670551-r1 { fill: #e0e0e0;font-weight: bold } -.terminal-1293670551-r2 { fill: #e0e0e0 } -.terminal-1293670551-r3 { fill: #c5c8c6 } -.terminal-1293670551-r4 { fill: #ddedf9;font-weight: bold } + .terminal-r1 { fill: #e0e0e0;font-weight: bold } +.terminal-r2 { fill: #c5c8c6 } +.terminal-r3 { fill: #ddedf9;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DataTableHotReloadingApp + DataTableHotReloadingApp - - - -  A           B     - one         two   - three       four  - five        six   - - - - - - - - - - - - - - - - - - - + + + +  A           B     + one         two   + three       four  + five        six   + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg index c207da3cb4..26d705a735 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-2406117197-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2406117197-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2406117197-r1 { fill: #e0e0e0;font-weight: bold } -.terminal-2406117197-r2 { fill: #e0e0e0 } -.terminal-2406117197-r3 { fill: #c5c8c6 } -.terminal-2406117197-r4 { fill: #ddedf9;font-weight: bold } + .terminal-r1 { fill: #e0e0e0;font-weight: bold } +.terminal-r2 { fill: #c5c8c6 } +.terminal-r3 { fill: #e0e0e0 } +.terminal-r4 { fill: #ddedf9;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + TableApp - - - -  lane  swimmer               country        time   - 4     Joseph Schooling      Singapore      50.39  - 2     Michael Phelps        United States  51.14  - 5     Chad le Clos          South Africa   51.14  - 6     László Cseh           Hungary        51.14  - 3     Li Zhuhao             China          51.26  - 8     Mehdy Metella         France         51.58  - 7     Tom Shields           United States  51.73  - 1     Aleksandr Sadovnikov  Russia         51.84  - - - - - - - - - - - - - - + + + +  lane  swimmer               country        time   + 4     Joseph Schooling      Singapore      50.39  + 2     Michael Phelps        United States  51.14  + 5     Chad le Clos          South Africa   51.14  + 6     László Cseh           Hungary        51.14  + 3     Li Zhuhao             China          51.26  + 8     Mehdy Metella         France         51.58  + 7     Tom Shields           United States  51.73  + 1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + From 0a5528f6551979a9df786c707f580ea64bbb120a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 29 Jan 2026 17:39:00 +0000 Subject: [PATCH 4/8] bump --- CHANGELOG.md | 7 ++++--- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649032a770..b4ec7ccd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased -### Changed +### [7.5.0] - 2026-01-29 -- The DataTable row cursor will extend to the full width if there is excess space -- The DataTable will send a selected event on click, only if the cell / row / column is currently highlighted +- The DataTable row cursor will extend to the full width if there is excess space https://github.com/Textualize/textual/pull/6345 +- The DataTable will send a selected event on click, only if the cell / row / column is currently highlighted https://github.com/Textualize/textual/pull/6345 ## [7.4.0] - 2026-01-25 @@ -3335,6 +3335,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[7.5.0]: https://github.com/Textualize/textual/compare/v7.4.0...v7.5.0 [7.4.0]: https://github.com/Textualize/textual/compare/v7.3.0...v7.4.0 [7.3.0]: https://github.com/Textualize/textual/compare/v7.2.0...v7.3.0 [7.2.0]: https://github.com/Textualize/textual/compare/v7.1.0...v7.2.0 diff --git a/pyproject.toml b/pyproject.toml index 02d2ee9c33..9479fc774d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "7.4.0" +version = "7.5.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" From fefec041759b6a48f5c675d388e973a22a0f8b3d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 29 Jan 2026 18:06:49 +0000 Subject: [PATCH 5/8] Update src/textual/widgets/_data_table.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/textual/widgets/_data_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 065132fdce..f7a3a6b2b3 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -329,7 +329,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): background: $surface; color: $foreground; height: auto; - max-height: 100%; + max-height: 100%; &.datatable--fixed-cursor { background: $block-cursor-blurred-background; } From a06a834dcf031fa9220620e4139027d88cb33bd0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Jan 2026 13:04:37 +0000 Subject: [PATCH 6/8] fix out of bounds logic --- src/textual/widgets/_data_table.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index f7a3a6b2b3..b921933217 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -2557,7 +2557,11 @@ def _on_mouse_move(self, event: events.MouseMove): and column metadata from the segments present in the cells.""" self._set_hover_cursor(True) meta = event.style.meta - if not meta or meta.get("out_of_bounds"): + if not meta: + self._set_hover_cursor(False) + return + + if self.cursor_type != "row" and meta.get("out_of_bounds", False): self._set_hover_cursor(False) return From d5a4eb2910928f148678a05c7399fefc69674260 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Jan 2026 13:07:24 +0000 Subject: [PATCH 7/8] out of bounds logic --- src/textual/widgets/_data_table.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index b921933217..dd5f9c6d72 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -2671,6 +2671,8 @@ async def _on_click(self, event: events.Click) -> None: meta = event.style.meta if "row" not in meta or "column" not in meta: return + if self.cursor_type != "row" and meta.get("out_of_bounds", False): + return row_index = meta["row"] column_index = meta["column"] From e0e904fb10f510bddcfa01c89ab77471dc5ea4e6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Jan 2026 13:08:55 +0000 Subject: [PATCH 8/8] fix docstring --- tests/test_data_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index f23373def7..6750c5ddbc 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -788,8 +788,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate(): async def test_datatable_click_cell_cursor(): - """When the cell cursor is used, and we click, we emit a CellHighlighted - *and* a CellSelected message for the cell that was clicked. + """When the cell cursor is used, and we click, we emit a CellHighlighted message, + a second click emits a CellSelected message. Regression test for https://github.com/Textualize/textual/issues/1723""" app = DataTableApp() async with app.run_test() as pilot: