89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import List
|
|
|
|
from PIL import ImageDraw
|
|
|
|
from .item import ListItem
|
|
|
|
|
|
class SelectableList(ABC):
|
|
"""Abstract base for list and carousel selection widgets.
|
|
|
|
Manages a collection of :class:`ListItem` objects with a single
|
|
*selected_index*, wrapping navigation, and a shared highlight-bar
|
|
drawing primitive. Concrete subclasses (:class:`ListView`,
|
|
:class:`CarouselView`) only differ in how they render the items.
|
|
|
|
The two subclasses expose an identical public interface so they can
|
|
be swapped interchangeably.
|
|
"""
|
|
|
|
def __init__(self,
|
|
items: List[ListItem] | None = None,
|
|
x_offset: int = 40,
|
|
max_visible: int = 5,
|
|
item_height: int = 20):
|
|
self._items: List[ListItem] = list(items or [])
|
|
self.selected_index: int = 0
|
|
self.x_offset = x_offset
|
|
self.max_visible = max_visible
|
|
self.item_height = item_height
|
|
|
|
# ---- item access ------------------------------------------------
|
|
|
|
@property
|
|
def items(self) -> List[ListItem]:
|
|
return self._items
|
|
|
|
@items.setter
|
|
def items(self, value: List[ListItem]):
|
|
self._items = list(value)
|
|
if self._items and self.selected_index >= len(self._items):
|
|
self.selected_index = len(self._items) - 1
|
|
elif not self._items:
|
|
self.selected_index = 0
|
|
|
|
@property
|
|
def selected_item(self) -> ListItem | None:
|
|
if self._items and 0 <= self.selected_index < len(self._items):
|
|
return self._items[self.selected_index]
|
|
return None
|
|
|
|
# ---- navigation -------------------------------------------------
|
|
|
|
def select_previous(self):
|
|
"""Move selection to the previous item (wraps around)."""
|
|
if self._items:
|
|
self.selected_index = (self.selected_index - 1) % len(self._items)
|
|
|
|
def select_next(self):
|
|
"""Move selection to the next item (wraps around)."""
|
|
if self._items:
|
|
self.selected_index = (self.selected_index + 1) % len(self._items)
|
|
|
|
# ---- drawing helpers --------------------------------------------
|
|
|
|
def _draw_highlight(self,
|
|
draw: ImageDraw.ImageDraw,
|
|
x: int,
|
|
y_center: int,
|
|
font_size: int = 10,
|
|
width: int = 90):
|
|
"""Draw a rounded highlight bar via overlapping circles.
|
|
|
|
The bar radius is derived from *font_size* so the bar always
|
|
fits snugly around the rendered text.
|
|
"""
|
|
r = font_size // 2 + 4
|
|
for i in range(0, width, max(1, r // 2)):
|
|
draw.circle((x + i, y_center), r, fill='black')
|
|
|
|
# ---- rendering (subclass) ---------------------------------------
|
|
|
|
@abstractmethod
|
|
def render(self, draw: ImageDraw.ImageDraw, y_start: int) -> None:
|
|
"""Render the widget onto *draw* starting at *y_start*."""
|
|
...
|