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:
Dave Pearson
2023-05-18 13:00:23 +01:00
parent 9d0a6d8eac
commit 0c18839c8a

View File

@@ -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()