From 048bd0c3b5c4d9d90c2efc73e2e1bbee13a4315a Mon Sep 17 00:00:00 2001 From: mar77i Date: Mon, 27 Jan 2025 16:16:08 +0100 Subject: [PATCH] super-slim basic modality --- ui/ui.py | 182 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 154 insertions(+), 28 deletions(-) diff --git a/ui/ui.py b/ui/ui.py index 27c2035..d5b7f1d 100644 --- a/ui/ui.py +++ b/ui/ui.py @@ -10,29 +10,18 @@ import pygame # 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 @@ -139,7 +128,7 @@ class UIChild(EventMethodDispatcher): @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 @@ -372,24 +361,25 @@ class Switch(UIChild): 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: @@ -409,11 +399,11 @@ class Cursor(EventMethodDispatcher): @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: @@ -485,9 +475,9 @@ class Cursor(EventMethodDispatcher): 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): @@ -504,7 +494,7 @@ class Cursor(EventMethodDispatcher): self.repeat_ts = None def key_blur(self, restore=False): - self.child.blur(restore) + self.text_input.blur(restore) KEY_METHODS = { frozenset(set()): { @@ -587,9 +577,9 @@ class TextInput(UIChild): 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) @@ -661,3 +651,139 @@ class IconButton(Button): 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) -- 2.51.0