Repository: Textualize/textual

Issue:
Mouse selection bug with Tab characters in TextArea with soft_wrap disabled
The following code creates 2 TextAreas pre-populated with 20 Tab characters.

```python3
from textual.app import App, ComposeResult
from textual.widgets import TextArea

class MyApp(App):

    def compose(self) -> ComposeResult:
        yield TextArea(
            id='textarea1',
            soft_wrap=False,
            text="\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
        )
        yield TextArea(
            id='textarea2',
            soft_wrap=True,
            text="\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
        )

if __name__ == "__main__":
    MyApp().run()
```
The first TextArea has soft_wrap disabled, while the second one does not.

By using a mouse to select the Tab characters, the selection in the first TextArea incorrectly jumps forward 1 tab character per space. The selection in the second TextArea follows the expected behavior.

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.85.2 |
| Rich    | 13.9.4 |

## Python

| Name           | Value                                  |
|----------------|----------------------------------------|
| Version        | 3.12.0                                 |
| Implementation | CPython                                |
| Compiler       | MSC v.1935 64 bit (AMD64)              |
| Executable     | C:\Program Files\Python312\python3.exe |

## Operating System

| Name    | Value      |
|---------|------------|
| System  | Windows    |
| Release | 10         |
| Version | 10.0.19045 |

## 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=188, height=52 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 188                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 52                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76279be25..b82e5c51f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Fixed
 
 - Fixed duplicated key displays in the help panel https://github.com/Textualize/textual/issues/5037
+- Fixed `TextArea` mouse selection with tab characters https://github.com/Textualize/textual/issues/5212
 
 ### Added
 
diff --git a/src/textual/document/_wrapped_document.py b/src/textual/document/_wrapped_document.py
index cef7a57dd..68846beda 100644
--- a/src/textual/document/_wrapped_document.py
+++ b/src/textual/document/_wrapped_document.py
@@ -295,7 +295,9 @@ class WrappedDocument:
         if not self._width:
             # No wrapping, so we directly map offset to location and clamp.
             line_index = min(y, len(self._wrap_offsets) - 1)
-            column_index = min(x, len(self.document.get_line(line_index)))
+            column_index = cell_width_to_column_index(
+                self.document.get_line(line_index), x, self._tab_width
+            )
             return line_index, column_index
 
         # Find the line corresponding to the given y offset in the wrapped document.


Test patch:
diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py
index 8b5424d2f..0efc9ca62 100644
--- a/tests/text_area/test_selection.py
+++ b/tests/text_area/test_selection.py
@@ -334,3 +334,22 @@ async def test_cursor_screen_offset_and_terminal_cursor_position_scrolling():
 
         assert text_area.cursor_screen_offset == (5, 1)
         assert app.cursor_position == (5, 1)
+
+
+async def test_mouse_selection_with_tab_characters():
+    """Regression test for https://github.com/Textualize/textual/issues/5212"""
+
+    class TextAreaTabsApp(App):
+        def compose(self) -> ComposeResult:
+            yield TextArea(soft_wrap=False, text="\t\t")
+
+    app = TextAreaTabsApp()
+    async with app.run_test() as pilot:
+        text_area = pilot.app.query_one(TextArea)
+        expected_selection = Selection((0, 0), (0, 0))
+        assert text_area.selection == expected_selection
+
+        await pilot.mouse_down(text_area, offset=(2, 1))
+        await pilot.hover(text_area, offset=(3, 1))
+
+        assert text_area.selection == expected_selection