Files

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*."""
...