Skip to content

Commit 45475d5

Browse files
authored
Merge pull request #6186 from Textualize/select-cells-fix
fix for selecting cells
2 parents 0b2b47d + 8498718 commit 45475d5

File tree

5 files changed

+47
-11
lines changed

5 files changed

+47
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Fixed
1111

1212
- Fixed type hint aliasing for App under TYPE_CHECKING https://github.com/Textualize/textual/pull/6152
13+
- Fixed for text selection with double width characters https://github.com/Textualize/textual/pull/6186
1314

1415
### Changed
1516

src/textual/_compositor.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,10 @@ def get_widget_and_offset_at(
927927
offset_x = 0
928928
offset_x2 = 0
929929

930+
from rich.cells import get_character_cell_size
931+
930932
for segment in line:
931-
end += len(segment.text)
933+
end += segment.cell_length
932934
style = segment.style
933935
if style is not None and style._meta is not None:
934936
meta = style.meta
@@ -937,11 +939,14 @@ def get_widget_and_offset_at(
937939
offset_x2 = offset_x + len(segment.text)
938940

939941
if x < end and x >= start:
940-
if x == end - 1:
941-
segment_offset = len(segment.text)
942-
else:
943-
first, _ = segment.split_cells(x - start)
944-
segment_offset = len(first.text)
942+
segment_cell_length = 0
943+
cell_cut = x - start
944+
segment_offset = 0
945+
for character in segment.text:
946+
if segment_cell_length >= cell_cut:
947+
break
948+
segment_cell_length += get_character_cell_size(character)
949+
segment_offset += 1
945950
return widget, (
946951
None
947952
if offset_y is None

src/textual/screen.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,14 +1652,22 @@ def _forward_event(self, event: events.Event) -> None:
16521652
):
16531653
end_widget = self._select_end[0]
16541654
select_offset = end_widget.content_region.bottom_right_inclusive
1655-
self._select_end = (end_widget, event.offset, select_offset)
1655+
self._select_end = (
1656+
end_widget,
1657+
event.screen_offset,
1658+
select_offset,
1659+
)
16561660

16571661
elif (
16581662
select_widget is not None
16591663
and select_widget.allow_select
16601664
and select_offset is not None
16611665
):
1662-
self._select_end = (select_widget, event.offset, select_offset)
1666+
self._select_end = (
1667+
select_widget,
1668+
event.screen_offset,
1669+
select_offset,
1670+
)
16631671

16641672
elif isinstance(event, events.MouseEvent):
16651673
if isinstance(event, events.MouseUp):
@@ -1730,7 +1738,6 @@ def _watch__select_end(
17301738
Args:
17311739
select_end: The end selection.
17321740
"""
1733-
17341741
if select_end is None or self._select_start is None:
17351742
# Nothing to select
17361743
return

src/textual/selection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ def extract(self, text: str) -> str:
5656
return lines[start_line][start_offset:end_offset]
5757

5858
selection: list[str] = []
59-
selected_lines = lines[start_line:end_line]
59+
selected_lines = lines[start_line : end_line + 1]
6060
if len(selected_lines) >= 2:
6161
first_line, *mid_lines, last_line = selected_lines
6262
selection.append(first_line[start_offset:])
6363
selection.extend(mid_lines)
64-
selection.append(last_line[: end_offset + 1])
64+
selection.append(last_line[:end_offset])
6565
else:
6666
return lines[start_line][start_offset:end_offset]
6767
return "\n".join(selection)

tests/test_selection.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pytest
22

3+
from textual.app import App, ComposeResult
34
from textual.geometry import Offset
45
from textual.selection import Selection
6+
from textual.widgets import Static
57

68

79
@pytest.mark.parametrize(
@@ -20,3 +22,24 @@
2022
def test_extract(text: str, selection: Selection, expected: str) -> None:
2123
"""Test Selection.extract"""
2224
assert selection.extract(text) == expected
25+
26+
27+
async def test_double_width():
28+
"""Test that selection works with double width characters."""
29+
30+
TEXT = """😂❤️👍Select😊🙏😍\nme🔥💯😭😂❤️👍"""
31+
32+
class TextSelectApp(App):
33+
def compose(self) -> ComposeResult:
34+
yield Static(TEXT)
35+
36+
app = TextSelectApp()
37+
async with app.run_test() as pilot:
38+
await pilot.pause()
39+
assert await pilot.mouse_down(offset=(2, 0))
40+
await pilot.pause()
41+
assert await pilot.mouse_up(offset=(7, 1))
42+
selected_text = app.screen.get_selected_text()
43+
expected = "❤️👍Select😊🙏😍\nme🔥💯😭"
44+
45+
assert selected_text == expected

0 commit comments

Comments
 (0)