diff --git a/CHANGELOG.md b/CHANGELOG.md index 6caa1e9249..1549114319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed type hint aliasing for App under TYPE_CHECKING https://github.com/Textualize/textual/pull/6152 +- Fixed for text selection with double width characters https://github.com/Textualize/textual/pull/6186 ### Changed diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 2346cb5e1c..c4e240e2e9 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -927,8 +927,10 @@ def get_widget_and_offset_at( offset_x = 0 offset_x2 = 0 + from rich.cells import get_character_cell_size + for segment in line: - end += len(segment.text) + end += segment.cell_length style = segment.style if style is not None and style._meta is not None: meta = style.meta @@ -937,11 +939,14 @@ def get_widget_and_offset_at( offset_x2 = offset_x + len(segment.text) if x < end and x >= start: - if x == end - 1: - segment_offset = len(segment.text) - else: - first, _ = segment.split_cells(x - start) - segment_offset = len(first.text) + segment_cell_length = 0 + cell_cut = x - start + segment_offset = 0 + for character in segment.text: + if segment_cell_length >= cell_cut: + break + segment_cell_length += get_character_cell_size(character) + segment_offset += 1 return widget, ( None if offset_y is None diff --git a/src/textual/screen.py b/src/textual/screen.py index c130af0a1a..8d13ac67be 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1652,14 +1652,22 @@ def _forward_event(self, event: events.Event) -> None: ): end_widget = self._select_end[0] select_offset = end_widget.content_region.bottom_right_inclusive - self._select_end = (end_widget, event.offset, select_offset) + self._select_end = ( + end_widget, + event.screen_offset, + select_offset, + ) elif ( select_widget is not None and select_widget.allow_select and select_offset is not None ): - self._select_end = (select_widget, event.offset, select_offset) + self._select_end = ( + select_widget, + event.screen_offset, + select_offset, + ) elif isinstance(event, events.MouseEvent): if isinstance(event, events.MouseUp): @@ -1730,7 +1738,6 @@ def _watch__select_end( Args: select_end: The end selection. """ - if select_end is None or self._select_start is None: # Nothing to select return diff --git a/src/textual/selection.py b/src/textual/selection.py index 464d218f85..0466fbec5a 100644 --- a/src/textual/selection.py +++ b/src/textual/selection.py @@ -56,12 +56,12 @@ def extract(self, text: str) -> str: return lines[start_line][start_offset:end_offset] selection: list[str] = [] - selected_lines = lines[start_line:end_line] + selected_lines = lines[start_line : end_line + 1] if len(selected_lines) >= 2: first_line, *mid_lines, last_line = selected_lines selection.append(first_line[start_offset:]) selection.extend(mid_lines) - selection.append(last_line[: end_offset + 1]) + selection.append(last_line[:end_offset]) else: return lines[start_line][start_offset:end_offset] return "\n".join(selection) diff --git a/tests/test_selection.py b/tests/test_selection.py index 13aaa5d33b..83fca7653d 100644 --- a/tests/test_selection.py +++ b/tests/test_selection.py @@ -1,7 +1,9 @@ import pytest +from textual.app import App, ComposeResult from textual.geometry import Offset from textual.selection import Selection +from textual.widgets import Static @pytest.mark.parametrize( @@ -20,3 +22,24 @@ def test_extract(text: str, selection: Selection, expected: str) -> None: """Test Selection.extract""" assert selection.extract(text) == expected + + +async def test_double_width(): + """Test that selection works with double width characters.""" + + TEXT = """πŸ˜‚β€οΈπŸ‘SelectπŸ˜ŠπŸ™πŸ˜\nmeπŸ”₯πŸ’―πŸ˜­πŸ˜‚β€οΈπŸ‘""" + + class TextSelectApp(App): + def compose(self) -> ComposeResult: + yield Static(TEXT) + + app = TextSelectApp() + async with app.run_test() as pilot: + await pilot.pause() + assert await pilot.mouse_down(offset=(2, 0)) + await pilot.pause() + assert await pilot.mouse_up(offset=(7, 1)) + selected_text = app.screen.get_selected_text() + expected = "β€οΈπŸ‘SelectπŸ˜ŠπŸ™πŸ˜\nmeπŸ”₯πŸ’―πŸ˜­" + + assert selected_text == expected