Skip to content

Commit

Permalink
Merge pull request #2970 from davep/unbork-selection-list
Browse files Browse the repository at this point in the history
Fix a crash caused by a prompt being wider than a `SelectionList`
  • Loading branch information
davep authored Jul 19, 2023
2 parents 0b9b677 + fed1ed4 commit 2f055f6
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.31.0] - Unreleased

### Fixed

- Fixed a crash when a `SelectionList` had a prompt wider than itself https://github.com/Textualize/textual/issues/2900

## [0.30.0] - 2023-07-17

### Added
Expand Down
27 changes: 24 additions & 3 deletions src/textual/widgets/_option_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def __init__(
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
wrap: bool = True,
):
"""Initialise the option list.
Expand All @@ -307,13 +308,20 @@ def __init__(
id: The ID of the option list in the DOM.
classes: The CSS classes of the option list.
disabled: Whether the option list is disabled or not.
wrap: Should prompts be auto-wrapped?
"""
super().__init__(name=name, id=id, classes=classes, disabled=disabled)

# Internal refresh trackers. For things driven from on_idle.
self._needs_refresh_content_tracking = False
self._needs_to_scroll_to_highlight = False

self._wrap = wrap
"""Should we auto-wrap options?
If `False` options wider than the list will be truncated.
"""

self._contents: list[OptionListContent] = [
self._make_content(item) for item in content
]
Expand Down Expand Up @@ -449,6 +457,14 @@ def _clear_content_tracking(self) -> None:
# bit.
self._option_ids.clear()

def _left_gutter_width(self) -> int:
"""Returns the size of any left gutter that should be taken into account.
Returns:
The width of the left gutter.
"""
return 0

def _refresh_content_tracking(self, force: bool = False) -> None:
"""Refresh the various forms of option list content tracking.
Expand Down Expand Up @@ -477,13 +493,18 @@ def _refresh_content_tracking(self, force: bool = False) -> None:

# Set up for doing less property access work inside the loop.
lines_from = self.app.console.render_lines
options = self.app.console.options.update_width(
self.scrollable_content_region.width
)
add_span = self._spans.append
option_ids = self._option_ids
add_lines = self._lines.extend

# Adjust the options for our purposes.
options = self.app.console.options.update_width(
self.scrollable_content_region.width - self._left_gutter_width()
)
options.no_wrap = not self._wrap
if not self._wrap:
options.overflow = "ellipsis"

# Create a rule that can be used as a separator.
separator = Strip(lines_from(Rule(style=""))[0])

Expand Down
14 changes: 14 additions & 0 deletions src/textual/widgets/_selection_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def __init__(
id=id,
classes=classes,
disabled=disabled,
wrap=False,
)

@property
Expand Down Expand Up @@ -484,6 +485,19 @@ def _toggle_highlighted_selection(self) -> None:
if self.highlighted is not None:
self.toggle(self.get_option_at_index(self.highlighted))

def _left_gutter_width(self) -> int:
"""Returns the size of any left gutter that should be taken into account.
Returns:
The width of the left gutter.
"""
return len(
ToggleButton.BUTTON_LEFT
+ ToggleButton.BUTTON_INNER
+ ToggleButton.BUTTON_RIGHT
+ " "
)

def render_line(self, y: int) -> Strip:
"""Render a line in the display.
Expand Down
29 changes: 29 additions & 0 deletions tests/selection_list/test_over_wide_selections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""See https://github.com/Textualize/textual/issues/2900 for the reason behind these tests."""

from textual.app import App, ComposeResult
from textual.widgets import SelectionList


class SelectionListApp(App[None]):
"""Test selection list application."""

CSS = """
OptionList {
width: 20;
}
"""

def compose(self) -> ComposeResult:
yield SelectionList[int](*[(f"{n} " * 100, n) for n in range(10)])


async def test_over_wide_options() -> None:
"""Options wider than the widget should not be an issue."""
async with SelectionListApp().run_test() as pilot:
assert pilot.app.query_one(SelectionList).highlighted == 0
await pilot.pause()
assert pilot.app.query_one(SelectionList).highlighted == 0


if __name__ == "__main__":
SelectionListApp().run()

0 comments on commit 2f055f6

Please sign in to comment.