]> git.mar77i.info Git - zenbook_gui/commitdiff
super-slim basic modality
authormar77i <mar77i@protonmail.ch>
Mon, 27 Jan 2025 15:16:08 +0000 (16:16 +0100)
committermar77i <mar77i@protonmail.ch>
Mon, 27 Jan 2025 15:16:08 +0000 (16:16 +0100)
ui/ui.py

index 27c2035f2c19ea0092e2b6073aa63bbe7df41693..d5b7f1ddb42db0d5ec14de83998f3651fa306526 100644 (file)
--- 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)