mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Fix click on input with double width chars (#3066)
* Fix cursor position when clicking double-width char #2968 The cursor moved to the correct position only when clicking the first index of the character. We now check if the click happened inside a range of indices. * Update changelog #2968
This commit is contained in:
@@ -72,6 +72,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Fixed background refresh https://github.com/Textualize/textual/issues/3055
|
||||
- Fixed `SelectionList.clear_options` https://github.com/Textualize/textual/pull/3075
|
||||
- `MouseMove` events bubble up from widgets. `App` and `Screen` receive `MouseMove` events even if there's no Widget under the cursor. https://github.com/Textualize/textual/issues/2905
|
||||
- Fixed click on double-width char https://github.com/Textualize/textual/issues/2968
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -421,10 +421,11 @@ class Input(Widget, can_focus=True):
|
||||
cell_offset = 0
|
||||
_cell_size = get_character_cell_size
|
||||
for index, char in enumerate(self.value):
|
||||
if cell_offset >= click_x:
|
||||
cell_width = _cell_size(char)
|
||||
if cell_offset <= click_x < (cell_offset + cell_width):
|
||||
self.cursor_position = index
|
||||
break
|
||||
cell_offset += _cell_size(char)
|
||||
cell_offset += cell_width
|
||||
else:
|
||||
self.cursor_position = len(self.value)
|
||||
|
||||
|
||||
@@ -6,28 +6,61 @@ from textual.app import App, ComposeResult
|
||||
from textual.geometry import Offset
|
||||
from textual.widgets import Input
|
||||
|
||||
# A string containing only single-width characters
|
||||
TEXT_SINGLE = "That gum you like is going to come back in style"
|
||||
|
||||
# A string containing only double-width characters
|
||||
TEXT_DOUBLE = "こんにちは"
|
||||
|
||||
# A string containing both single and double-width characters
|
||||
TEXT_MIXED = "aこんbcにdちeは"
|
||||
|
||||
|
||||
class InputApp(App[None]):
|
||||
TEST_TEXT = "That gum you like is going to come back in style"
|
||||
def __init__(self, text):
|
||||
super().__init__()
|
||||
self._text = text
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Input(self.TEST_TEXT)
|
||||
yield Input(self._text)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"click_at, should_land",
|
||||
"text, click_at, should_land",
|
||||
(
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(10, 10),
|
||||
(len(InputApp.TEST_TEXT) - 1, len(InputApp.TEST_TEXT) - 1),
|
||||
(len(InputApp.TEST_TEXT), len(InputApp.TEST_TEXT)),
|
||||
(len(InputApp.TEST_TEXT) * 2, len(InputApp.TEST_TEXT)),
|
||||
# Single-width characters
|
||||
(TEXT_SINGLE, 0, 0),
|
||||
(TEXT_SINGLE, 1, 1),
|
||||
(TEXT_SINGLE, 10, 10),
|
||||
(TEXT_SINGLE, len(TEXT_SINGLE) - 1, len(TEXT_SINGLE) - 1),
|
||||
(TEXT_SINGLE, len(TEXT_SINGLE), len(TEXT_SINGLE)),
|
||||
(TEXT_SINGLE, len(TEXT_SINGLE) * 2, len(TEXT_SINGLE)),
|
||||
# Double-width characters
|
||||
(TEXT_DOUBLE, 0, 0),
|
||||
(TEXT_DOUBLE, 1, 0),
|
||||
(TEXT_DOUBLE, 2, 1),
|
||||
(TEXT_DOUBLE, 3, 1),
|
||||
(TEXT_DOUBLE, 4, 2),
|
||||
(TEXT_DOUBLE, 5, 2),
|
||||
(TEXT_DOUBLE, (len(TEXT_DOUBLE) * 2) - 1, len(TEXT_DOUBLE) - 1),
|
||||
(TEXT_DOUBLE, len(TEXT_DOUBLE) * 2, len(TEXT_DOUBLE)),
|
||||
(TEXT_DOUBLE, len(TEXT_DOUBLE) * 10, len(TEXT_DOUBLE)),
|
||||
# Mixed-width characters
|
||||
(TEXT_MIXED, 0, 0),
|
||||
(TEXT_MIXED, 1, 1),
|
||||
(TEXT_MIXED, 2, 1),
|
||||
(TEXT_MIXED, 3, 2),
|
||||
(TEXT_MIXED, 4, 2),
|
||||
(TEXT_MIXED, 5, 3),
|
||||
(TEXT_MIXED, 13, 9),
|
||||
(TEXT_MIXED, 14, 9),
|
||||
(TEXT_MIXED, 15, 10),
|
||||
(TEXT_MIXED, 1000, 10),
|
||||
),
|
||||
)
|
||||
async def test_mouse_clicks_within(click_at, should_land):
|
||||
async def test_mouse_clicks_within(text, click_at, should_land):
|
||||
"""Mouse clicks should result in the cursor going to the right place."""
|
||||
async with InputApp().run_test() as pilot:
|
||||
async with InputApp(text).run_test() as pilot:
|
||||
# Note the offsets to take into account the decoration around an
|
||||
# Input.
|
||||
await pilot.click(Input, Offset(click_at + 3, 1))
|
||||
@@ -37,7 +70,7 @@ async def test_mouse_clicks_within(click_at, should_land):
|
||||
|
||||
async def test_mouse_click_outwith():
|
||||
"""Mouse clicks outside the input should not affect cursor position."""
|
||||
async with InputApp().run_test() as pilot:
|
||||
async with InputApp(TEXT_SINGLE).run_test() as pilot:
|
||||
pilot.app.query_one(Input).cursor_position = 3
|
||||
assert pilot.app.query_one(Input).cursor_position == 3
|
||||
await pilot.click(Input, Offset(0, 0))
|
||||
|
||||
Reference in New Issue
Block a user