import pygame
-from ui.ui import Button, Child
+from ui import Button, Child
class DrawImage(Child):
- def __init__(self, root, rect, background_color=None, load_surf=None):
- super().__init__(root)
+ def __init__(self, parent, rect, background_color=None, load_surf=None):
+ super().__init__(parent)
self.pos = rect.topleft
self.surface = pygame.Surface(rect.size, 0, 24)
self.load_surf(load_surf, background_color)
class ColorButton(Button):
- def __init__(self, root, rect, color, callback, is_active=False):
- super().__init__(root, rect, None, callback, is_active)
+ def __init__(self, parent, rect, color, callback, is_active=False):
+ super().__init__(parent, rect, None, callback, is_active)
self.color = color
def draw(self):
from .label import Label
from .message_box import MessageBox
from .modal import Modal
+from .scroll import Scroll
from .slider import Slider
from .spinner import Spinner
from .switch import Switch
-from functools import partial
+from functools import cached_property, partial
import pygame
self.children = []
def handle_event(self, ev):
- for child in (super(), *self.children):
+ Child.handle_event(self, ev)
+ self.handle_event_children(ev)
+
+ def handle_event_children(self, ev):
+ root = self.root
+ if not root.running or root.stop_event:
+ return
+ for child in self.children:
child.handle_event(ev)
+ if not root.running or root.stop_event:
+ break
def update(self):
for child in self.children:
class Child(EventMethodDispatcher):
- root: "Root"
-
- def __init__(self, root):
- self.root = root
+ def __init__(self, parent):
+ self.parent = parent
+
+ @cached_property
+ def root(self):
+ parent = self.parent
+ while hasattr(parent, "parent"):
+ parent = parent.root or parent.parent
+ if not isinstance(parent, Root):
+ raise AttributeError(f"No root found for {self}")
+ return parent
@property
def dirty(self):
def dirty(self, value):
self.root.dirty = value
- @property
+ @cached_property
def font(self):
return self.root.font
- @property
+ @cached_property
def surf(self):
- return self.root.surf
+ parent = self.parent
+ while not hasattr(parent, "surf"):
+ parent = parent.root or parent.parent
+ return parent.surf
def draw(self):
pass
pass
+class ChildAndParent(Parent, Child):
+ def __init__(self, parent):
+ Parent.__init__(self)
+ Child.__init__(self, parent)
+
+
class Root(Parent):
BACKGROUND_COLOR: pygame.Color
def __init__(self, surf, font=None):
super().__init__()
- self.surf = surf
self.font = font
self.running = True
self.dirty = False
+ self.surf = surf
self.clock = pygame.time.Clock()
self.cursor = None
self.stop_event = False
+ self.root = self
def handle_quit(self, _):
self.running = False
KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}}
- def handle_event(self, ev):
- for child in (super(Parent, self), *self.children):
- child.handle_event(ev)
- if not self.running or self.stop_event:
- break
-
def draw(self):
if hasattr(self, "BACKGROUND_COLOR"):
self.surf.fill(self.BACKGROUND_COLOR)
class Button(Child):
- def __init__(self, root, rect, value, callback, is_active=False):
- super().__init__(root)
+ def __init__(self, parent, rect, value, callback, is_active=False):
+ super().__init__(parent)
self.rect = rect
self.value = value
self.callback = callback
class DropDownMenu(Modal):
- def __init__(self, root, rect, entries, callback):
- super().__init__(root)
+ def __init__(self, parent, rect, entries, callback):
+ super().__init__(parent)
self.callback = callback
self.children.extend(
(
Button(
- root,
+ self,
pygame.Rect(
(rect.left, rect.bottom + i * rect.height), rect.size,
),
class DropDown(Button):
- def __init__(self, root, rect, value, entries, callback, is_active=False):
- self.dropdown_menu = DropDownMenu(root, rect, entries, callback)
- super().__init__(root, rect, value, self.dropdown_menu.activate, is_active)
+ def __init__(self, parent, rect, value, entries, callback, is_active=False):
+ self.dropdown_menu = DropDownMenu(parent, rect, entries, callback)
+ super().__init__(parent, rect, value, self.dropdown_menu.activate, is_active)
class FPSWidget(Child):
FPS_COLOR = "yellow"
- def __init__(self, root):
- super().__init__(root)
- self.clock = self.root.clock
+ def __init__(self, parent):
+ super().__init__(parent)
self.current_fps = None
def update(self):
- new_fps = int(self.clock.get_fps())
+ new_fps = int(self.root.clock.get_fps())
if self.current_fps != new_fps:
self.current_fps = new_fps
self.dirty = True
class Icon(Child):
- def __init__(self, root, shape):
- super().__init__(root)
+ def __init__(self, parent, shape):
+ super().__init__(parent)
self.shape = shape
def draw(self):
class IconButton(Button):
- def __init__(self, root, shape, *args, **kwargs):
- super().__init__(root, *args, **kwargs)
+ def __init__(self, parent, shape, *args, **kwargs):
+ super().__init__(parent, *args, **kwargs)
self.shape = shape
def draw(self):
class Label(Child):
- def __init__(self, root, rect, value):
- super().__init__(root)
+ def __init__(self, parent, rect, value):
+ super().__init__(parent)
self.rect = rect
self.value = value
class MessageBox(Modal):
- def __init__(self, root, rect, message):
- super().__init__(root)
+ def __init__(self, parent, rect, message):
+ super().__init__(parent)
self.rect = rect
- self.label = Label(root, pygame.Rect(rect.center, (10, 10)), "")
+ self.label = Label(self.parent, pygame.Rect(rect.center, (10, 10)), "")
self.children.extend((self.label, *self.get_buttons()))
self.message = message
def get_buttons(self):
rect = self.rect
yield Button(
- self.root,
+ self.parent,
pygame.Rect(
(rect.left + rect.width // 3, rect.centery + 64),
(rect.width // 3, 128),
class Modal(Child):
- def __init__(self, root):
- super().__init__(root)
+ def __init__(self, parent):
+ super().__init__(parent)
self.backsurf = None
self.children = []
self.active = False
self.root.children.extend(children)
def activate(self):
- self.backsurf = self.tinted_copy(self.root.surf)
+ self.backsurf = self.tinted_copy(self.surf)
self.swap_children()
self.root.children.insert(0, self)
self.active = True
--- /dev/null
+import pygame
+
+from .base import ChildAndParent
+
+
+class Scroll(ChildAndParent):
+ def __init__(self, parent, rect, surf_size):
+ super().__init__(parent)
+ self.rect = rect
+ self.surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32)
+ self.scroll_x = 0
+ self.scroll_y = 0
+
+ def get_subsurf(self):
+ size = self.surf.get_size()
+ return self.surf.subsurface(
+ pygame.Rect(
+ (self.scroll_x, self.scroll_y),
+ (
+ min(self.rect.width, size[0] - self.scroll_x),
+ min(self.rect.height, size[1] - self.scroll_y),
+ )
+ )
+ )
+
+ def draw(self):
+ super().draw()
+ pygame.draw.rect(self.parent.surf, "gray", self.rect)
+ self.root.surf.blit(self.get_subsurf(), self.rect.topleft)
+
+ def handle_event_children(self, ev):
+ if hasattr(ev, "pos"):
+ if not self.rect.collidepoint(ev.pos):
+ return
+ ev = pygame.event.Event(
+ ev.type,
+ {
+ **ev.__dict__,
+ "pos": (
+ ev.pos[0] - self.rect.left + self.scroll_x,
+ ev.pos[1] - self.rect.top + self.scroll_y,
+ )
+ },
+ )
+ super().handle_event_children(ev)
HORIZONTAL = 0
VERTICAL = 1
- def __init__(self, root, rect, direction, value=0, callback=None):
- super().__init__(root)
+ def __init__(self, parent, rect, direction, value=0, callback=None):
+ super().__init__(parent)
self.rect = rect
self.direction = direction
self.extent = (self.rect.width - 1, self.rect.height - 1)[direction]
import pygame
-from .base import Child, Parent
+from .base import ChildAndParent
from .button import Button
from .text_input import TextInput
DELAY_MS = 500
REPEAT_MS = 100
- def __init__(self, root, rect, value, callback, is_active=False):
+ def __init__(self, parent, rect, value, callback, is_active=False):
self._pushed = False
- super().__init__(root, rect, value, callback, is_active)
+ super().__init__(parent, rect, value, callback, is_active)
self.repeat_ts = None
@property
self.dirty = True
-class Spinner(Parent, Child):
- def __init__(self, root, rect, callback, value=0):
- Parent.__init__(self)
- Child.__init__(self, root)
+class Spinner(ChildAndParent):
+ def __init__(self, parent, rect, callback, value=0):
+ super().__init__(parent)
self.callback = callback
self.value = value
button_size = (rect.height // 2, rect.height // 2)
self.children.extend(
(
TextInput(
- root,
+ self,
pygame.Rect(
rect.topleft, (rect.width - button_size[0], rect.height)
),
re.compile(r"[-+]?\d*").fullmatch,
),
RepeatButton(
- root,
+ self,
pygame.Rect(
(rect.right - button_size[0], rect.top),
button_size,
partial(self.spin_callback, 1),
),
RepeatButton(
- root,
+ self,
pygame.Rect(
(rect.right - button_size[0], rect.top + button_size[1]),
button_size,
MOVE_FOR_SEC = 1
EASE = staticmethod(EaseInOutElastic((sqrt(5) - 1) / 2))
- def __init__(self, root, rect, callback, value=False):
- super().__init__(root)
+ def __init__(self, parent, rect, callback, value=False):
+ super().__init__(parent)
self.rect = rect
self.callback = callback
if value is not None and not isinstance(value, bool):
import pygame
-from .base import Child
+from .base import ChildAndParent
from .button import Button
-class TabBar(Child):
- def __init__(self, root, rect, labels, groups, active):
- super().__init__(root)
+class TabBar(ChildAndParent):
+ def __init__(self, parent, rect, labels, groups, active):
+ super().__init__(parent)
self.labels = labels
self.groups = groups
num_names = len(groups)
self.buttons = [
Button(
- root,
+ self,
pygame.Rect(
(rect.left + rect.width * i // num_names, rect.top),
(rect.width // num_names, rect.height),
)
for i in range(len(groups))
]
- root.children.extend(self.buttons)
- root.children.extend(self.groups[active])
+ self.children.extend(self.buttons)
self.active = active
+ self.children.extend(self.groups[active])
def update_children(self, name):
if self.active == name:
button.is_active = is_group_active
self.dirty = True
for item in group:
- is_child_active = item in self.root.children
+ is_child_active = item in self.children
if is_group_active == is_child_active:
continue
if is_group_active:
- self.root.children.append(item)
+ self.children.append(item)
elif is_child_active:
- self.root.children.remove(item)
+ self.children.remove(item)
self.dirty = True
REPEAT_MS = 100
def __init__(self, text_input, x_offset):
- super().__init__(text_input.root)
+ super().__init__(text_input.parent)
self.text_input = text_input
self.old_value = text_input.value
self.key_callback = None
self.key = None
self.repeat_ts = None
self.pos = self.pos_from_offset(x_offset)
+ # I would rather have one cursor object on the current focus that activates
+ # itself by appending self to the current_focus stack.
self.root.children.append(self)
def remove(self):
class TextInput(Child):
- def __init__(self, root, rect, callback, value="", value_filter=None):
- super().__init__(root)
+ def __init__(self, parent, rect, callback, value="", value_filter=None):
+ super().__init__(parent)
self.rect = rect
self.callback = callback
self.value = value