]> git.mar77i.info Git - zenbook_gui/commitdiff
add switch
authormar77i <mar77i@protonmail.ch>
Mon, 20 Jan 2025 16:08:50 +0000 (17:08 +0100)
committermar77i <mar77i@protonmail.ch>
Tue, 21 Jan 2025 01:12:10 +0000 (02:12 +0100)
ui/ui.py

index 024c7054f5fb643a820d1c2c6cfd5b06542ce208..8f9e6dfe2cd485ce9d8c51d67f50aa66ef54f88e 100644 (file)
--- a/ui/ui.py
+++ b/ui/ui.py
@@ -1,6 +1,21 @@
+from colorsys import hsv_to_rgb
+from math import asin, nan, sin, sqrt, tau
+from time import time
+
 import pygame
 
 
+# todo:
+# - [ ] 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:
     def handle_event(self, ev):
         method_name = f"handle_{pygame.event.event_name(ev.type).lower()}"
@@ -31,7 +46,6 @@ class UIParent(EventMethodDispatcher):
     def handle_keydown(self, ev):
         if ev.key == pygame.K_ESCAPE:
             self.running = False
-            return
 
     def handle_event(self, ev):
         super().handle_event(ev)
@@ -96,28 +110,23 @@ class UIChild(EventMethodDispatcher):
 
 
 class Button(UIChild):
-    def __init__(self, parent, rect, label, callback, is_active=None):
+    def __init__(self, parent, rect, value, callback, is_active=False):
         super().__init__(parent)
         self.rect = rect
-        self.label = label
+        self.value = value
         self.callback = callback
         self.is_active = is_active
         self.pushed = False
 
     def draw(self):
         if not self.pushed:
-            label_color = (
-                "lime" if callable(self.is_active) and self.is_active() else "gray"
-            )
-            frame_color = label_color
+            value_color = "lime" if self.is_active else "gray"
+            frame_color = value_color
         else:
             pygame.draw.rect(self.parent.surf, "darkgray", self.rect)
             frame_color = "lightgray"
-            label_color = "black"
-        label = self.label
-        if callable(label):
-            label = label()
-        fs = self.parent.font.render(label, True, label_color)
+            value_color = "black"
+        fs = self.parent.font.render(self.value, True, value_color)
         pygame.draw.rect(self.parent.surf, frame_color, self.rect, 8)
         fs_size = fs.get_size()
         center = self.rect.center
@@ -142,3 +151,170 @@ class Button(UIChild):
         if ev.buttons[0] and not self.rect.collidepoint(ev.pos):
             self.pushed = False
             self.parent.dirty = True
+
+
+class Slider(UIChild):
+    HORIZONTAL = 0
+    VERTICAL = 1
+
+    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]
+        self.value = value
+        self.callback = callback
+        self.pushed = False
+
+    def update_value(self, value):
+        if self.direction == self.HORIZONTAL:
+            value -= self.rect.left
+        else:  # self.direction == self.VERTICAL
+            value = self.extent - (value - self.rect.top)
+        self.value = value
+        if self.callback:
+            self.callback(value)
+        self.parent.dirty = True
+
+    def handle_mousebuttondown(self, ev):
+        if ev.button != 1 or not self.rect.collidepoint(ev.pos):
+            return
+        self.update_value(ev.pos[self.direction])
+        self.pushed = True
+
+    def handle_mousemotion(self, ev):
+        if not self.pushed:
+            return
+        if not ev.buttons[0]:
+            self.pushed = False
+        else:
+            self.update_value(ev.pos[self.direction])
+
+    def handle_mousebuttonup(self, ev):
+        if ev.button == 1 and self.pushed:
+            self.update_value(ev.pos[self.direction])
+            self.pushed = False
+
+    def draw(self):
+        pygame.draw.rect(self.parent.surf, "gray34", self.rect.inflate((8, 8)))
+        pygame.draw.rect(self.parent.surf, "black", self.rect)
+        self.draw_cursor(self.parent.surf.subsurface(self.rect))
+
+    def draw_cursor(self, subsurf):
+        value = self.value
+        color = "gray"
+        if value < 0:
+            color = "dimgray"
+            value = -value
+        if value > self.extent:
+            value = int(self.extent * (self.extent / value))
+            color = "dimgray"
+        if self.direction == self.HORIZONTAL:
+            start_pos, end_pos = (value, 0), (value, self.rect.height)
+        else:
+            value = self.extent - value
+            start_pos, end_pos = (0, value), (self.rect.width, value)
+        pygame.draw.line(subsurf, color, start_pos, end_pos, 8)
+
+
+class Label(UIChild):
+    def __init__(self, parent, rect, value):
+        super().__init__(parent)
+        self.rect = rect
+        self.value = value
+
+    def draw(self):
+        fs = self.parent.font.render(self.value, True, "gray")
+        self.parent.surf.blit(
+            fs,
+            (self.rect.left + 16, self.rect.centery - fs.get_height() // 2)
+        )
+
+
+class EaseInOutElastic:
+    def __init__(self, magnitude):
+        self.p = 1 - magnitude
+        self.s = self.p / tau * asin(1)
+
+    def __call__(self, x):
+        if x == 0:
+            return 0
+        elif x == 1:
+            return 1
+        elif x < 0 or x > 1:
+            raise ValueError(f"x must be between 0 and 1: got {x}")
+        st = x * 2
+        st1 = st - 1
+        sgn = (st >= 1) * 2 - 1
+        return (
+            2 ** (-sgn * 10 * st1 - 1)
+            * sin((st1 - self.s) * tau / self.p)
+            * sgn
+            + (sgn > 0)
+        )
+
+
+class Switch(UIChild):
+    MOVE_FOR_SEC = 1
+    EASE = staticmethod(EaseInOutElastic((sqrt(5) - 1) / 2))
+
+    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):
+            value = bool(value)
+        self.value = value
+        self.moving_since = nan
+        self.flip_again = False
+
+    def draw(self):
+        pygame.draw.rect(self.parent.surf, "gray", self.rect, 8)
+        t = time()
+        if t > self.moving_since + self.MOVE_FOR_SEC:
+            self.callback(self.value)
+            if self.flip_again:
+                self.value = bool(self.value) ^ True
+                self.moving_since = t
+                self.flip_again = False
+            else:
+                self.moving_since = nan
+        if self.moving_since is nan:
+            if self.value is None:
+                current = 0.5
+            else:
+                current = min(max(int(self.value), 0), 1)
+        else:
+            current = (t - self.moving_since) / self.MOVE_FOR_SEC
+            if not self.value:
+                current = 1 - current
+        eased_current = self.EASE(current)
+        base_radius = min(self.rect.height, self.rect.width / 4)
+        base_left = self.rect.left + base_radius
+        movement_width = self.rect.width - 2 * base_radius
+        normalized_current = min(max(current, 0), 1)
+        if current < 0.5:
+            args = (0, 1 - 2 * normalized_current, 1 - normalized_current)
+        else:
+            normalized_current -= 0.5
+            args = (1 / 3, 2 * normalized_current, 0.5 + normalized_current * 0.5)
+        rgb = hsv_to_rgb(*args)
+        pygame.draw.circle(
+            self.parent.surf,
+            pygame.Color(*(int(x * 255) for x in rgb)),
+            (base_left + eased_current * movement_width, self.rect.top + base_radius),
+            base_radius
+        )
+
+    def update(self):
+        if self.moving_since is not nan:
+            self.parent.dirty = True
+
+    def handle_mousebuttondown(self, ev):
+        if ev.button == 1 and self.rect.collidepoint(ev.pos):
+            if self.moving_since is not nan:
+                self.flip_again = True
+                return
+            self.value = bool(self.value) ^ True
+            offset = self.MOVE_FOR_SEC / 2 if self.value is None else 0
+            self.moving_since = time() - offset