]> git.mar77i.info Git - zenbook_gui/commitdiff
scroll field, but without scrollbars
authormar77i <mar77i@protonmail.ch>
Mon, 10 Feb 2025 01:55:42 +0000 (02:55 +0100)
committermar77i <mar77i@protonmail.ch>
Mon, 10 Feb 2025 01:55:42 +0000 (02:55 +0100)
17 files changed:
bookpaint/draw_ui.py
ui/__init__.py
ui/base.py
ui/button.py
ui/drop_down.py
ui/fps_widget.py
ui/icon.py
ui/icon_button.py
ui/label.py
ui/message_box.py
ui/modal.py
ui/scroll.py [new file with mode: 0644]
ui/slider.py
ui/spinner.py
ui/switch.py
ui/tab_bar.py
ui/text_input.py

index eb19e125a416f089f54e97408f9385204e8befc2..6dd724d694f809fe9084720e2bb712c4e9e4be8f 100644 (file)
@@ -1,11 +1,11 @@
 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)
@@ -54,8 +54,8 @@ class DrawImage(Child):
 
 
 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):
index c07599a8f61e148c883a3f3c1f202496284bcb6d..affe335fe41d78eac9e82764141b5ed95ec9693d 100644 (file)
@@ -7,6 +7,7 @@ from .icon_button import IconButton
 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
index 6fe79da0ae5f69967f60814dc0aa7517550d703b..fc86575bbe6101f788e1df0d02fc32058634be23 100644 (file)
@@ -1,4 +1,4 @@
-from functools import partial
+from functools import cached_property, partial
 
 import pygame
 
@@ -35,8 +35,17 @@ class Parent(EventMethodDispatcher):
         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:
@@ -48,10 +57,17 @@ class Parent(EventMethodDispatcher):
 
 
 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):
@@ -61,13 +77,16 @@ class Child(EventMethodDispatcher):
     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
@@ -76,18 +95,25 @@ class Child(EventMethodDispatcher):
         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
@@ -103,12 +129,6 @@ class Root(Parent):
 
     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)
index 9c2c14a6731e6b387d3157c3faab5e485e6006c0..3d6286f3f7e385f41f86656b995cb6c6ce9c06e6 100644 (file)
@@ -4,8 +4,8 @@ from .base import Child
 
 
 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
index d6446795d9f1e089166279fc1950ba71e1af27fb..b09ec680dc2f0b521a5d85ecd59f7964f12e1197 100644 (file)
@@ -7,13 +7,13 @@ from .modal import Modal
 
 
 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,
                     ),
@@ -39,6 +39,6 @@ class DropDownMenu(Modal):
 
 
 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)
index 472efa848899cb39b0a72f889d75d58e30af7cef..d14866bd5ad1a595e0c9829bca5b082fa96028fb 100644 (file)
@@ -4,13 +4,12 @@ from .base import Child
 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
index 35ec63391ddf64c2636cb864073a8a43c059edb0..9e3becb10d946fafeac2798147de05a9d6d35a02 100644 (file)
@@ -2,8 +2,8 @@ from .base import Child
 
 
 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):
index ebb077a87f681029380f6d0bd68667c7aaf644b6..9b952a6e9c36a5e02a00855af96e3ec7b4dcb5df 100644 (file)
@@ -4,8 +4,8 @@ from .button import Button
 
 
 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):
index d8cd65c91b4a437680cf6a9ef16649430a0c0a0f..20de04cf451e0ffe3fe03f397dd1ce9578eb7e92 100644 (file)
@@ -2,8 +2,8 @@ from .base import Child
 
 
 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
 
index f95c3460cca3b4403f243a1a8a5b0ab3529e78b5..3c431527bc413809fb015b09221f89cd56b96eff 100644 (file)
@@ -6,17 +6,17 @@ from .modal import Modal
 
 
 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),
index ef7bba46aee52c870f704646532347091852725f..0a1b4ef07b052714a56cc1b560df17e6ae64aab0 100644 (file)
@@ -4,8 +4,8 @@ from .base import Child
 
 
 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
@@ -41,7 +41,7 @@ class Modal(Child):
         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
diff --git a/ui/scroll.py b/ui/scroll.py
new file mode 100644 (file)
index 0000000..22c7614
--- /dev/null
@@ -0,0 +1,45 @@
+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)
index aa516d1267df1b752471ea119cf84f3d76bddc8e..5f1b510a59e2e795f7f1cf2aab73dac6bfc2547a 100644 (file)
@@ -7,8 +7,8 @@ class Slider(Child):
     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]
index 066af4bf097c94c92e4d4c9f268f54cc1554a14e..95740010a5a7b47a2f23b65fb865b3c489a3975f 100644 (file)
@@ -5,7 +5,7 @@ from time import time
 
 import pygame
 
-from .base import ChildParent
+from .base import ChildAndParent
 from .button import Button
 from .text_input import TextInput
 
@@ -14,9 +14,9 @@ class RepeatButton(Button):
     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
@@ -49,17 +49,16 @@ class RepeatButton(Button):
             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)
                     ),
@@ -68,7 +67,7 @@ class Spinner(Parent, Child):
                     re.compile(r"[-+]?\d*").fullmatch,
                 ),
                 RepeatButton(
-                    root,
+                    self,
                     pygame.Rect(
                         (rect.right - button_size[0], rect.top),
                         button_size,
@@ -77,7 +76,7 @@ class Spinner(Parent, Child):
                     partial(self.spin_callback, 1),
                 ),
                 RepeatButton(
-                    root,
+                    self,
                     pygame.Rect(
                         (rect.right - button_size[0], rect.top + button_size[1]),
                         button_size,
index 69e5d6d72f805971f0d61a0c86f06be3cdfcc314..634ca685cb00c5882387d2a3376ec16fab9cfb84 100644 (file)
@@ -34,8 +34,8 @@ class Switch(Child):
     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):
index 8fafa308a778f796bd2e0ec19898faa8ce44b0a5..180785c8902033e4206e505c92ae4f75097488c0 100644 (file)
@@ -2,19 +2,19 @@ from functools import partial
 
 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),
@@ -25,9 +25,9 @@ class TabBar(Child):
             )
             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:
@@ -39,11 +39,11 @@ class TabBar(Child):
                 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
index 4e3a08a5d3fc3719149f58c5c7de1ce9e7b222f6..6481b5133dc7d85ee2a46f1f84bd4f3a18517337 100644 (file)
@@ -13,13 +13,15 @@ class Cursor(Child):
     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):
@@ -230,8 +232,8 @@ class Cursor(Child):
 
 
 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