Source code for gale.ui.list_view

"""
This file contains the implementation of the class ListView: a
vertical list of selectable (label, on_select) items, navigable by
mouse or keyboard/gamepad.

Author: Alejandro Mujica (aledrums@gmail.com)
"""

from typing import Callable, List, Optional, Sequence, Tuple

import pygame

from gale.input_handler import MouseClickData
from gale.text import Text

from .cursor import Cursor
from .theme import Theme
from .widget import Direction, Widget

Item = Tuple[str, Callable[[], None]]


[docs] class ListView(Widget): """ A vertical list of selectable items, each a (label, on_select) pair. Supports both keyboard navigation (on_navigate moves the selection with wraparound, on_confirm invokes the selected item) and mouse interaction (hovering highlights, clicking selects and invokes). This is the generalization of gale.ui's "menu": build a menu as Container([Panel(...), ListView(...)]). Usage example: menu = ListView( 40, 40, 240, 120, items=[("Host", start_hosting), ("Join", start_joining), ("Quit", quit_game)], ) """ def __init__( self, x: float, y: float, width: float, height: float, items: Sequence[Item], font: Optional[pygame.font.Font] = None, cursor: Optional[Cursor] = None, theme: Optional[Theme] = None, ) -> None: """ :param x: The list's x position. :param y: The list's y position. :param width: The list's width. :param height: The list's height. :param items: The (label, on_select) pairs to show, top to bottom. :param font: The font used for each item's label. The default value is None, so theme.font is used. :param cursor: A Cursor rendered next to the selected item. The default value is None, so no indicator is drawn (the row highlight alone marks the selection). :param theme: This widget's own theme. The default value is None. """ super().__init__(x, y, width, height, theme=theme) self.items: List[Item] = list(items) self.selected_index: int = 0 self._font = font self.cursor: Optional[Cursor] = cursor self.focusable = True
[docs] def row_height(self) -> float: return self.height / len(self.items) if self.items else self.height
[docs] def row_rect(self, index: int) -> pygame.Rect: row_height = self.row_height() return pygame.Rect( int(self.x), int(self.y + index * row_height), int(self.width), int(row_height), )
[docs] def render(self, surface: pygame.Surface) -> None: if not self.visible or not self.items: return font = self._font if self._font is not None else self.theme.font for index, (label, _) in enumerate(self.items): row_rect = self.row_rect(index) if index == self.selected_index: fill_color = ( self.theme.focus_color if self.focused else self.theme.hover_color ) pygame.draw.rect(surface, fill_color, row_rect) text_obj = Text( label, font, row_rect.centerx, row_rect.centery, self.theme.text_color, center=True, ) text_obj.render(surface) if index == self.selected_index and self.cursor is not None: self.cursor.render(surface, (row_rect.x - 4, row_rect.centery))
[docs] def on_mouse_motion(self, position: Tuple[float, float]) -> None: self.hovered = self.contains(position) if self.hovered: self.selected_index = self._row_at(position)
[docs] def on_mouse_click( self, position: Tuple[float, float], data: MouseClickData ) -> bool: if not self.enabled or not self.contains(position): return False if data.released: self.selected_index = self._row_at(position) self._activate() return True
[docs] def on_confirm(self) -> bool: if not self.enabled or not self.items: return False self._activate() return True
[docs] def on_navigate(self, direction: Direction) -> bool: if not self.enabled or not self.items: return False _, dy = direction if dy == 0: return False self.selected_index = (self.selected_index + dy) % len(self.items) return True
def _row_at(self, position: Tuple[float, float]) -> int: row_height = self.row_height() index = int((position[1] - self.y) // row_height) if row_height else 0 return max(0, min(len(self.items) - 1, index)) def _activate(self) -> None: _, on_select = self.items[self.selected_index] on_select()