Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 11 additions & 6 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/textual/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_selection.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
Loading