mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
WiP selection list
I think I'm going to give up on basing this off OptionList. It's close enough that inheriting from it and doing more makes some sense, but it's also just far enough away that it's starting to feel like it's more work that is worthwhile and it'll be easier to hand-roll something fresh.
This commit is contained in:
@@ -2,11 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
from typing import ClassVar, Generic, TypeVar
|
||||
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style
|
||||
from rich.text import Text, TextType
|
||||
|
||||
from ..binding import Binding
|
||||
from ._option_list import Option, OptionList
|
||||
from ._toggle_button import ToggleButton
|
||||
|
||||
SelectionType = TypeVar("SelectionType")
|
||||
"""The type for the value of a `Selection`"""
|
||||
@@ -17,9 +21,9 @@ class Selection(Generic[SelectionType], Option):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: SelectionList,
|
||||
value: SelectionType,
|
||||
prompt: RenderableType,
|
||||
selected: bool = False,
|
||||
prompt: TextType,
|
||||
id: str | None = None,
|
||||
disabled: bool = False,
|
||||
):
|
||||
@@ -28,26 +32,68 @@ class Selection(Generic[SelectionType], Option):
|
||||
Args:
|
||||
value: The value for the selection.
|
||||
prompt: The prompt for the selection.
|
||||
selected: The initial selected state for the selection.
|
||||
selected: Is this particular selection selected?
|
||||
id: The optional ID for the selection.
|
||||
disabled: The initial enabled/disabled state. Enabled by default.
|
||||
"""
|
||||
self._prompt = prompt
|
||||
self._parent = parent
|
||||
super().__init__(prompt, id, disabled)
|
||||
self._value: SelectionType = value
|
||||
self._selected: bool = selected
|
||||
|
||||
@property
|
||||
def value(self) -> SelectionType:
|
||||
"""The value for this selection."""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def prompt(self) -> RenderableType:
|
||||
return self._parent._make_label(self)
|
||||
|
||||
@property
|
||||
def selected(self) -> bool:
|
||||
"""The selected state of this selection."""
|
||||
return self._selected
|
||||
return self._value in self._parent._selected
|
||||
|
||||
|
||||
class SelectionList(Generic[SelectionType], OptionList):
|
||||
"""A vertical option list that allows making multiple selections."""
|
||||
|
||||
BINDINGS = [Binding("space, enter", "toggle"), Binding("x", "redraw")]
|
||||
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||
"selection-list--button",
|
||||
"selection-list--button-selected",
|
||||
}
|
||||
|
||||
DEFAULT_CSS = """
|
||||
/* Base button colours (including in dark mode). */
|
||||
|
||||
SelectionList > .selection-list--button {
|
||||
color: $background;
|
||||
text-style: bold;
|
||||
background: $foreground 15%;
|
||||
}
|
||||
|
||||
SelectionList:focus > .selection-list--button {
|
||||
background: $foreground 25%;
|
||||
background: red;
|
||||
color: red;
|
||||
}
|
||||
|
||||
SelectionList > .selection-list--button-selected {
|
||||
color: $success;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
SelectionList:focus > .selection-list--button-selected {
|
||||
background: $foreground 25%;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*selections: Selection[SelectionType] | tuple[SelectionType, str],
|
||||
*selections: tuple[SelectionType, TextType]
|
||||
| tuple[SelectionType, TextType, bool],
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
@@ -63,15 +109,53 @@ class SelectionList(Generic[SelectionType], OptionList):
|
||||
disabled: Whether the selection list is disabled or not.
|
||||
"""
|
||||
super().__init__(
|
||||
*[self._make_selection(selection) for selection in selections],
|
||||
name=name,
|
||||
id=id,
|
||||
classes=classes,
|
||||
disabled=disabled,
|
||||
)
|
||||
self._selected: dict[SelectionType, None] = {}
|
||||
self._selections = selections
|
||||
|
||||
def _on_mount(self):
|
||||
self.add_options(
|
||||
[self._make_selection(selection) for selection in self._selections]
|
||||
)
|
||||
if self.option_count:
|
||||
self.highlighted = 0
|
||||
|
||||
def _make_label(self, selection: Selection) -> Text:
|
||||
# Grab the button style.
|
||||
button_style = self.get_component_rich_style(
|
||||
f"selection-list--button{'-selected' if selection.selected else ''}"
|
||||
)
|
||||
|
||||
# If the button is off, we're going to do a bit of a switcharound to
|
||||
# make it look like it's a "cutout".
|
||||
if not selection.selected:
|
||||
button_style += Style.from_color(
|
||||
self.background_colors[1].rich_color, button_style.bgcolor
|
||||
)
|
||||
|
||||
# Building the style for the side characters. Note that this is
|
||||
# sensitive to the type of character used, so pay attention to
|
||||
# BUTTON_LEFT and BUTTON_RIGHT.
|
||||
side_style = Style.from_color(
|
||||
button_style.bgcolor, self.background_colors[1].rich_color
|
||||
)
|
||||
|
||||
return Text.assemble(
|
||||
(ToggleButton.BUTTON_LEFT, side_style),
|
||||
(ToggleButton.BUTTON_INNER, button_style),
|
||||
(ToggleButton.BUTTON_RIGHT, side_style),
|
||||
" ",
|
||||
selection._prompt,
|
||||
)
|
||||
|
||||
def _make_selection(
|
||||
self, selection: Selection[SelectionType] | tuple[SelectionType, str]
|
||||
self,
|
||||
selection: tuple[SelectionType, TextType]
|
||||
| tuple[SelectionType, TextType, bool],
|
||||
) -> Selection[SelectionType]:
|
||||
"""Turn incoming selection data into a `Selection` instance.
|
||||
|
||||
@@ -81,4 +165,24 @@ class SelectionList(Generic[SelectionType], OptionList):
|
||||
Returns:
|
||||
An instance of a `Selection`.
|
||||
"""
|
||||
return selection if isinstance(selection, Selection) else Selection(*selection)
|
||||
if len(selection) == 3:
|
||||
value, label, selected = selection
|
||||
elif len(selection) == 2:
|
||||
value, label, selected = (*selection, False)
|
||||
else:
|
||||
# TODO: Proper error.
|
||||
raise TypeError("Wrong number of values for a selection.")
|
||||
if selected:
|
||||
self._selected[value] = None
|
||||
return Selection(self, value, label)
|
||||
|
||||
def action_toggle(self) -> None:
|
||||
if self.highlighted is not None:
|
||||
option = self.get_option_at_index(self.highlighted)
|
||||
assert isinstance(option, Selection)
|
||||
if option.selected:
|
||||
del self._selected[option._value]
|
||||
else:
|
||||
self._selected[option._value] = None
|
||||
self._refresh_content_tracking(force=True)
|
||||
self.refresh()
|
||||
|
||||
Reference in New Issue
Block a user