Repository: Textualize/textual

Issue:
Hovering over an empty DataTable row does not reset hover effect
_Have you checked closed issues? https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed_

Yes, I have.

_Please give a brief but clear explanation of the issue. If you can, include a complete working example that demonstrates the bug. **Check it can run without modifications.**_

To reproduce the issue, run the below code and hover the cursor above one of the cells. Then, move the cursor down into the empty space below, and observe how the orange highlighting on the cell persists.

```py
from textual.app import App
from textual.widgets import DataTable

class ExampleApp(App):
    def compose(self):
        table = DataTable()
        table.add_columns('foo', 'bar', 'baz')
        table.add_row('foo', 'bar', 'baz')
        table.styles.height = '100%'
        yield table

ExampleApp().run()
```

_It will be helpful if you run the following command and paste the results:_

```
textual diagnose
```

<!-- This is valid Markdown, do not quote! -->
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.27.0 |
| Rich    | 13.4.2 |

## Python

| Name           | Value                                                              |
|----------------|--------------------------------------------------------------------|
| Version        | 3.11.3                                                             |
| Implementation | CPython                                                            |
| Compiler       | MSC v.1934 64 bit (AMD64)                                          |
| Executable     | C:\Users\Andrew\AppData\Local\Programs\Python\Python311\python.exe |

## Operating System

| Name    | Value      |
|---------|------------|
| System  | Windows    |
| Release | 10         |
| Version | 10.0.22621 |

## Terminal

| Name                 | Value            |
|----------------------|------------------|
| Terminal Application | Windows Terminal |
| TERM                 | *Not set*        |
| COLORTERM            | *Not set*        |
| FORCE_COLOR          | *Not set*        |
| NO_COLOR             | *Not set*        |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=120, height=30 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 120                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 30                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

_Feel free to add screenshots and / or videos. These can be very helpful!_

https://github.com/Textualize/textual/assets/26030336/78da926b-915c-46c2-b817-5c9a6e16eb7a



Discussion:
We found the following entry in the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) which you may find helpful:


- [Why doesn't the `DataTable` scroll programmatically?](https://github.com/textualize/textual/blob/main/FAQ.md#why-doesn't-the-`datatable`-scroll-programmatically)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
I thought this might be a quick fix, but I can't seem to unset the hover cursor similar to when focus leaves the widget.

https://github.com/Textualize/textual/blob/26573b9cffa0be57ffcab35bab9a7fe84ae8a063/src/textual/widgets/_data_table.py#L1979-L1992

A simple workaround would be to set the table height to auto and wrap it inside a container.
I've been looking at this issue again as it feels like it should be such an easy fix.

If the mouse isn't hovering a cell in the table, then there's no `meta` info. But I can't seem to conditionally unset the hover cursor no matter what I try, so there must be something I'm missing.

@darrenburns do you have any ideas? Sorry I normally wouldn't tag individuals, but it seems like you're the DataTable expert!
I think you're looking at the correct code, but there looks like a cacheing bug too, which is preventing the update from being visible.

The cell render cache key doesn't include `self._show_hover_cursor`, which means subsequent repaints of the cell containing the hover cursor will get the old cached rendered cell (with hover cursor highlighting), regardless of whether `self._show_hover_cursor` is `True` or `False`.

Adding `self._show_hover_cursor` to the cell render cache_key (inside `render_cell`), combined with the changes you suggested above, should do the trick.

Resolving patch:
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -1757,6 +1757,7 @@ def _render_cell(
             base_style,
             cursor,
             hover,
+            self._show_hover_cursor,
             self._update_count,
             self._pseudo_class_state,
         )
@@ -2076,8 +2077,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:
+            self._set_hover_cursor(False)
+            return
 
-        if meta and self.show_cursor and self.cursor_type != "none":
+        if self.show_cursor and self.cursor_type != "none":
             try:
                 self.hover_coordinate = Coordinate(meta["row"], meta["column"])
             except KeyError:


Test patch:
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -1137,3 +1137,25 @@ async def test_move_cursor():
         assert table.cursor_coordinate == Coordinate(3, 6)
         table.move_cursor(column=3)
         assert table.cursor_coordinate == Coordinate(3, 3)
+
+
+async def test_unset_hover_highlight_when_no_table_cell_under_mouse():
+    """When there isn't a table cell under the mouse cursor, there should be no
+    hover highlighting.
+
+    Regression test for #2909 https://github.com/Textualize/textual/issues/2909
+    """
+    app = DataTableApp()
+    async with app.run_test() as pilot:
+        table = app.query_one(DataTable)
+        table.add_column("ABC")
+        table.add_row("123")
+        await pilot.pause()
+        assert table.hover_coordinate == Coordinate(0, 0)
+        # Hover over a cell, and the hover cursor is visible
+        await pilot.hover(DataTable, offset=Offset(1, 1))
+        assert table._show_hover_cursor
+        # Move our cursor so it is outside any table cells but still within
+        # the widget, and the hover cursor is hidden
+        await pilot.hover(DataTable, offset=Offset(42, 1))
+        assert not table._show_hover_cursor