# todo:
# - [ ] textinput place cursor next to the mouse
# - [ ] spinner
-# - [ ] modal dialogs
-# - modal dialog will always replace all children, if you want persistent
-# controls, you can use a custom list
-# - [ ] tab bar
-# - [ ] vector label
-# - [ ] vector button
-# - [ ]
class EventMethodDispatcher:
MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT)
KEY_METHODS = {}
- @classmethod
- def get_mods(cls, mod):
+ def get_key_method(self, key, mod):
mods = set()
- for mask in cls.MODS:
+ for mask in self.MODS:
if mod & mask:
mods.add(mask)
- return frozenset(mods)
-
- def get_key_method(self, key, mod):
- method = self.KEY_METHODS.get(self.get_mods(mod), {}).get(key)
+ method = self.KEY_METHODS.get(frozenset(mods), {}).get(key)
if method is not None:
return partial(method, self)
return None
@property
def cursor(self):
cursor = self.parent.cursor
- if cursor is not None and cursor.child is self:
+ if cursor is not None and cursor.text_input is self:
return cursor
return None
self.set_value(bool(self.value) ^ True)
-class Cursor(EventMethodDispatcher):
+class Cursor(UIChild, EventMethodDispatcher):
DELAY_MS = 500
REPEAT_MS = 100
- def __init__(self, child):
- self.child = child
- self.old_value = child.value
+ def __init__(self, text_input):
+ 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 = len(child.value)
+ self.pos = len(text_input.value)
@contextmanager
def check_dirty(self):
old = self.pos, self.value
yield
if (self.pos, self.value) != old:
- self.child.dirty = True
+ self.text_input.dirty = True
def handle_keydown(self, ev):
if (key_method := self.get_key_method(ev.key, ev.mod)) is not None:
@property
def value(self):
- return self.child.value
+ return self.text_input.value
@value.setter
def value(self, value):
- self.child.value = value
+ self.text_input.value = value
def update(self):
if self.key_callback is None:
value = self.value
len_old_value = len(value)
value = "".join((value[:self.pos], unicode, value[self.pos:]))
- if self.child.value_filter:
+ if self.text_input.value_filter:
try:
- result = self.child.value_filter(value)
+ result = self.text_input.value_filter(value)
except Exception:
result = None
if isinstance(result, str):
self.repeat_ts = None
def key_blur(self, restore=False):
- self.child.blur(restore)
+ self.text_input.blur(restore)
KEY_METHODS = {
frozenset(set()): {
def focus(self):
cursor = self.cursor
if cursor is not None:
- if cursor.child is self:
+ if cursor.text_input is self:
return
- cursor.child.blur(True)
+ cursor.text_input.blur(True)
self.dirty = True
self.cursor = Cursor(self)
else:
color = "gray"
pygame.draw.rect(self.parent.surf, color, self.rect, 8)
+
+
+class TabBar(UIChild):
+ def __init__(self, parent, rect, labels, groups, active):
+ super().__init__(parent)
+ self.labels = labels
+ self.groups = groups
+ num_names = len(groups)
+ self.buttons = [
+ Button(
+ parent,
+ pygame.Rect(
+ (rect.left + rect.width * i // num_names, rect.top),
+ (rect.width // num_names, rect.height),
+ ),
+ labels[i],
+ partial(self.update_children, i),
+ i == active,
+ )
+ for i in range(len(groups))
+ ]
+ parent.children.extend(self.buttons)
+ parent.children.extend(self.groups[active])
+ self.active = active
+
+ def update_children(self, name):
+ if self.active == name:
+ return
+ self.active = name
+ for i, (button, group) in enumerate(zip(self.buttons, self.groups)):
+ is_group_active = i == self.active
+ if button.is_active != is_group_active:
+ button.is_active = is_group_active
+ self.dirty = True
+ for item in group:
+ is_child_active = item in self.parent.children
+ if is_group_active == is_child_active:
+ continue
+ if is_group_active:
+ self.parent.children.append(item)
+ elif is_child_active:
+ self.parent.children.remove(item)
+ self.dirty = True
+
+
+class Modal(UIChild):
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.backsurf = parent.surf.copy()
+ self.tintsurf = pygame.Surface(self.backsurf.get_size(), pygame.SRCALPHA, 32)
+ self.tintsurf.fill(pygame.Color(0x00000080))
+ self.children = parent.children.copy()
+ parent.children.clear()
+ parent.children.append(self)
+ self.dirty = True
+
+ def draw(self):
+ self.surf.blit(self.backsurf, (0, 0))
+ self.surf.blit(self.tintsurf, (0, 0))
+
+ def deactivate(self):
+ self.parent.children.clear()
+ self.parent.children.extend(self.children)
+ self.dirty = True
+
+
+class DropDown(Button):
+ def __init__(self, parent, rect, value, entries, callback):
+ super().__init__(parent, rect, value, partial(self.DropDownMenu, self), False)
+ self.entries = entries
+ self.dropdown_callback = callback
+
+ class DropDownMenu(Modal):
+ def __init__(self, sibling):
+ parent = sibling.parent
+ super().__init__(parent)
+ self.callback = sibling.dropdown_callback
+ rect = sibling.rect
+ self.buttons = [
+ Button(
+ parent,
+ pygame.Rect(
+ (rect.left, rect.bottom + i * rect.height), rect.size,
+ ),
+ entry,
+ partial(self.choose, i),
+ )
+ for i, entry in enumerate(sibling.entries)
+ ]
+ parent.children.extend(self.buttons)
+
+ def choose(self, i=None):
+ self.deactivate()
+ self.callback(i)
+
+ def handle_mousebuttondown(self, ev):
+ if ev.button != 1:
+ return
+ elif not any(b.rect.collidepoint(ev.pos) for b in self.buttons):
+ self.deactivate()
+
+
+class MessageBox(Modal):
+ def __init__(self, parent, rect, message):
+ super().__init__(parent)
+ self.rect = rect
+ fs_size = self.font.size(message)
+ label_rect = pygame.Rect(
+ rect.topleft,
+ (rect.width, rect.height * 3 // 4),
+ )
+ parent.children.extend(
+ (
+ Label(
+ parent,
+ pygame.Rect(
+ (label_rect.centerx - fs_size[0] // 2, label_rect.centery - fs_size[1] // 2),
+ fs_size,
+ ),
+ message,
+ ),
+ Button(
+ parent,
+ pygame.Rect(
+ (rect.left + rect.width // 3, rect.top + rect.height * 6 // 8),
+ (rect.width // 3, rect.height // 8),
+ ),
+ "OK",
+ self.deactivate,
+ ),
+ ),
+ )
+
+ def draw(self):
+ super().draw()
+ pygame.draw.rect(self.surf, "black", self.rect)