Source code for gale.ui.container

"""
This file contains the implementation of the class Container: a
scene-graph node holding any number of child widgets, dispatching
input to them (mouse hit-testing, keyboard focus/navigation) and
rendering/updating them in add order.

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

from typing import List, Optional, Sequence, Tuple

import pygame

from gale.input_handler import MouseClickData

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


[docs] class Container(Widget): """ A real scene-graph node holding any number of child widgets. Rendered/updated in add order (which is also z-order: to bring a widget to front, remove and re-add it). Mouse events hit-test children in reverse add order (top-most first) and stop at the first one that consumes the event. on_confirm/on_navigate are forwarded only to the currently focused child; an unconsumed on_navigate instead moves focus to the next focusable sibling. Usage example: menu = Container(40, 40, 240, 160, children=[ Panel(40, 40, 240, 160), ListView(48, 48, 224, 144, items=[("Host", start_hosting), ("Join", start_joining)]), ]) """ def __init__( self, x: float, y: float, width: float, height: float, children: Optional[Sequence[Widget]] = None, theme: Optional[Theme] = None, visible: bool = True, ) -> None: super().__init__(x, y, width, height, theme=theme, visible=visible) self.children: List[Widget] = [] for child in children or []: self.add_child(child) if self._focusable_children() and not any( child.focused for child in self.children ): self._focus_first() @property def focusable(self) -> bool: return any(child.focusable for child in self.children)
[docs] def add_child(self, widget: Widget) -> None: widget.parent = self self.children.append(widget)
[docs] def remove_child(self, widget: Widget) -> None: if widget in self.children: widget.parent = None self.children.remove(widget)
[docs] def update(self, dt: float) -> None: for child in self.children: if child.visible: child.update(dt)
[docs] def render(self, surface: pygame.Surface) -> None: if not self.visible: return for child in self.children: if child.visible: child.render(surface)
[docs] def on_mouse_motion(self, position: Tuple[float, float]) -> None: self.hovered = self.contains(position) for child in self.children: if child.visible and child.enabled: child.on_mouse_motion(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 for child in reversed(self.children): if not child.visible or not child.enabled: continue if child.on_mouse_click(position, data): self._focus_only(child) return True return False
[docs] def on_confirm(self) -> bool: focused_child = self._focused_child() return focused_child.on_confirm() if focused_child is not None else False
[docs] def on_navigate(self, direction: Direction) -> bool: focused_child = self._focused_child() if focused_child is not None and focused_child.on_navigate(direction): return True return self._move_focus(direction)
def _focusable_children(self) -> List[Widget]: return [ child for child in self.children if child.visible and child.enabled and child.focusable ] def _focused_child(self) -> Optional[Widget]: for child in self.children: if child.focused: return child return None def _focus_first(self) -> None: focusable = self._focusable_children() if focusable: focusable[0].focused = True def _focus_only(self, widget: Widget) -> None: for child in self.children: child.focused = child is widget def _move_focus(self, direction: Direction) -> bool: focusable = self._focusable_children() if not focusable: return False _, dy = direction if dy == 0: return False current = self._focused_child() current_index = focusable.index(current) if current in focusable else -1 next_index = (current_index + dy) % len(focusable) self._focus_only(focusable[next_index]) return True