]> git.mar77i.info Git - zenbook_gui/commitdiff
revamp bookpaint. some more improvements in ui, too
authormar77i <mar77i@protonmail.ch>
Fri, 16 May 2025 06:55:58 +0000 (08:55 +0200)
committermar77i <mar77i@protonmail.ch>
Fri, 16 May 2025 06:55:58 +0000 (08:55 +0200)
18 files changed:
bookpaint.py
bookpaint/base_menu.py [new file with mode: 0644]
bookpaint/bookpaint.py
bookpaint/color_menu.py
bookpaint/draw_image.py
bookpaint/line_menu.py
bookpaint/page_menu.py
connect_four.py
create_desktop_file.sh
launch/__init__.py [new file with mode: 0644]
memory.py
rps.py
ui/event_method_dispatcher.py
ui/slider.py
ui/spinner.py
ui/utils.py [deleted file]
vs_memory.py
zenbook_conf.py

index cba34b71b7f91d154e2affd86fa0cfbbe588b3a5..39415ea0acec3add4bef1894a8320d4e38ad0b34 100755 (executable)
@@ -1,16 +1,6 @@
 #!/usr/bin/env python3
 
-import sys
-from contextlib import redirect_stdout
-from io import StringIO
-from pathlib import Path
+from launch import run
 
-sys.path.append(str(Path(__file__).parent))
-
-from bookpaint.bookpaint import BookPaint
-
-with redirect_stdout(StringIO()):
-    # ruff: noqa: F401
-    import pygame  # type: ignore
-
-BookPaint().run()
+if __name__ == "__main__":
+    run()
diff --git a/bookpaint/base_menu.py b/bookpaint/base_menu.py
new file mode 100644 (file)
index 0000000..0411177
--- /dev/null
@@ -0,0 +1,10 @@
+import pygame
+
+from ui import Modal
+
+
+class BaseMenu(Modal):
+    def handle_quit(self, _):
+        self.root.running = False
+
+    key_methods = {frozenset(): {pygame.K_ESCAPE: Modal.deactivate}}
index addb0d02aff4a8b9f82884b899a8d67db037bcff..002531d4a7478c5ad6eb11c008bb68a065fa326c 100644 (file)
@@ -28,12 +28,12 @@ class BookPaint(Root):
             pygame.display.set_mode((0, 0), pygame.FULLSCREEN),
             pygame.font.Font(None, size=96),
         )
+        self.draw_image = DrawImage(self, pygame.Color("white"))
         self.color_menu = ColorMenu(self)
-        self.draw_image = DrawImage(self, "white")
         self.line_menu = LineMenu(self)
+        self.book_manager = BookManager(self, Path("book"))
         self.page_menu = PageMenu(self)
         self.menu = Menu(self)
-        self.book_manager = BookManager(self, Path("book"))
         self.page_menu.update_label()
         self.setup_deactivate_keys()
 
index 4067db6d2ee7bb0b12963fc989b8e0ba4aa06b1a..992ce637bcfe8f5f4b72b5925bd1145006666284 100644 (file)
+from colorsys import hsv_to_rgb, rgb_to_hsv
+from functools import partial
+
 import pygame
 
-from ui import Button, Label, Modal, TextInput
+from ui import Child, Label, Slider, Spinner, TextInput
+
+from .base_menu import BaseMenu
+
 
+class ColorLabel(Child):
+    def __init__(self, parent, rect, color):
+        super().__init__(parent)
+        self.rect = rect
+        self.color = color
+
+    def draw(self):
+        pygame.draw.rect(self.surf, self.color, self.rect)
 
-class ColorMenu(Modal):
+
+class ColorMenu(BaseMenu):
     def __init__(self, parent):
         super().__init__(parent)
         size = self.surf.get_size()
-        self.size_third = tuple(a // 3 for a in size)
+        self.modal_rect = pygame.Rect(
+            (size[0] // 8, size[1] // 8),
+            (size[0] * 3 // 4, size[1] * 3 // 4),
+        )
+        y = self.modal_rect.top
         button_height = 128
-        y = self.size_third[1]
         Label(
             self,
-            pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)),
+            pygame.Rect(
+                (self.modal_rect.left, y), (self.modal_rect.width, button_height)
+            ),
             "Color Menu",
             Label.HAlign.CENTER,
         )
+        label_width = self.modal_rect.width // 16
+        slider_width = self.modal_rect.width * 5 // 16
+        input_width = self.modal_rect.width * 2 // 16
+        label_left = self.modal_rect.left
+        slider_left = label_left + label_width
+        input_left = slider_left + slider_width
+        label_right = self.modal_rect.centerx
+        slider_right = label_right + label_width
+        input_right = slider_right + slider_width
+
+        y += button_height * 2
+        Label(
+            self,
+            pygame.Rect((label_left, y), (label_width, button_height)),
+            "R",
+            Label.HAlign.CENTER,
+        )
+        self.red_slider = Slider(
+            self,
+            pygame.Rect((slider_left, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_rgb, dest="r", sender_type=Slider),
+        )
+        self.red_input = Spinner(
+            self,
+            pygame.Rect((input_left, y), (input_width, button_height)),
+            partial(self.set_rgb, dest="r", sender_type=Spinner),
+        )
+        Label(
+            self,
+            pygame.Rect((label_right, y), (label_width, button_height)),
+            "H",
+            Label.HAlign.CENTER,
+        )
+        self.hue_slider = Slider(
+            self,
+            pygame.Rect((slider_right, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_hsv, dest=0, sender_type=Slider),
+        )
+        self.hue_input = Spinner(
+            self,
+            pygame.Rect((input_right, y), (input_width, button_height)),
+            partial(self.set_hsv, dest=0, sender_type=Spinner),
+        )
+
+        y += button_height + 16
+        Label(
+            self,
+            pygame.Rect((label_left, y), (label_width, button_height)),
+            "G",
+            Label.HAlign.CENTER,
+        )
+        self.green_slider = Slider(
+            self,
+            pygame.Rect((slider_left, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_rgb, dest="g", sender_type=Slider),
+        )
+        self.green_input = Spinner(
+            self,
+            pygame.Rect((input_left, y), (input_width, button_height)),
+            partial(self.set_rgb, dest="g", sender_type=Spinner),
+        )
+        Label(
+            self,
+            pygame.Rect((label_right, y), (label_width, button_height)),
+            "S",
+            Label.HAlign.CENTER,
+        )
+        self.saturation_slider = Slider(
+            self,
+            pygame.Rect((slider_right, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_hsv, dest=1, sender_type=Slider),
+        )
+        self.saturation_input = Spinner(
+            self,
+            pygame.Rect((input_right, y), (input_width, button_height)),
+            partial(self.set_hsv, dest=1, sender_type=Spinner),
+        )
+
         y += button_height + 16
-        self.input = TextInput(
+        Label(
+            self,
+            pygame.Rect((label_left, y), (label_width, button_height)),
+            "B",
+            Label.HAlign.CENTER,
+        )
+        self.blue_slider = Slider(
+            self,
+            pygame.Rect((slider_left, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_rgb, dest="b", sender_type=Slider),
+        )
+        self.blue_input = Spinner(
+            self,
+            pygame.Rect((input_left, y), (input_width, button_height)),
+            partial(self.set_rgb, dest="b", sender_type=Spinner),
+        )
+        Label(
+            self,
+            pygame.Rect((label_right, y), (label_width, button_height)),
+            "V",
+            Label.HAlign.CENTER,
+        )
+        self.value_slider = Slider(
+            self,
+            pygame.Rect((slider_right, y), (slider_width, button_height)),
+            Slider.Direction.HORIZONTAL,
+            0,
+            button_height,
+            partial(self.set_hsv, dest=2, sender_type=Slider),
+        )
+        self.value_input = Spinner(
+            self,
+            pygame.Rect((input_right, y), (input_width, button_height)),
+            partial(self.set_hsv, dest=2, sender_type=Spinner),
+        )
+
+        y += button_height * 2
+        half_width = self.modal_rect.width // 2
+        self.hex_input = TextInput(
             self,
-            pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)),
+            pygame.Rect(
+                (self.modal_rect.left + 128, y), (half_width - 256, button_height)
+            ),
             self.set_color,
             "",
         )
-        y += (button_height + 16) * 2
-        Button(
+        self.color_label = ColorLabel(
             self,
-            pygame.Rect((self.size_third[0], y), (self.size_third[0], 128)),
-            "Close",
-            self.deactivate
+            pygame.Rect(
+                (self.modal_rect.centerx + 128, y),
+                (half_width - 256, button_height),
+            ),
+            pygame.Color("white"),
         )
 
     def activate(self):
         super().activate()
-        self.input.value = f"{int(self.parent.draw_image.color) >> 8:06x}"
+        self.update_controls()
 
-    def set_color(self, value):
+    @staticmethod
+    def color_to_hsv(color):
+        return tuple(
+            max(min(x * 255, 255), 0)
+            for x in rgb_to_hsv(color.r / 255, color.g / 255, color.b / 255)
+        )
+
+    @staticmethod
+    def hsv_to_color(hsv):
+        return pygame.Color(
+            *(int(c * 255) for c in hsv_to_rgb(*(x / 255 for x in hsv)))
+        )
+
+    def update_controls(self, skip_hsv=False):
         color = self.parent.draw_image.color
-        try:
-            color = pygame.Color(f"0x{value}")
-        except ValueError:
-            try:
-                color = pygame.Color(value)
-            except ValueError:
-                pass
-        self.parent.draw_image.color = color
+        self.color_label.color = color
+        self.hex_input.value = f"{int(color) >> 8:06x}"
+        controls = (
+            (self.red_slider, self.red_input, color.r),
+            (self.green_slider, self.green_input, color.g),
+            (self.blue_slider, self.blue_input, color.b),
+        )
+        if not skip_hsv:
+            hsv = self.color_to_hsv(color)
+            controls = (
+                *controls,
+                (self.hue_slider, self.hue_input, int(hsv[0])),
+                (self.saturation_slider, self.saturation_input, int(hsv[1])),
+                (self.value_slider, self.value_input, int(hsv[2])),
+            )
+        for slider, spinner, value in controls:
+            slider.value = slider.extent * value // 255
+            spinner.value = value
 
     def draw_modal(self):
         super().draw_modal()
-        rect = pygame.Rect(self.size_third, self.size_third)
+        rect = self.modal_rect.copy()
         rect.inflate_ip((32, 32))
         pygame.draw.rect(self.surf, "black", rect)
         pygame.draw.rect(self.surf, "gray", rect, 1)
 
-    def draw(self):
-        super().draw()
-        pygame.draw.rect(
-            self.surf,
-            self.parent.draw_image.color,
-            pygame.Rect(
-                (self.size_third[0], self.size_third[1] + 288),
-                (self.size_third[0], 128),
-            ),
+    def set_rgb(self, value, dest, sender_type):
+        slider, spinner = {
+            "r": (self.red_slider, self.red_input),
+            "g": (self.green_slider, self.green_input),
+            "b": (self.blue_slider, self.blue_input),
+        }[dest]
+        if sender_type is Slider:
+            value = value * 255 // slider.extent
+        elif sender_type is Spinner:
+            pass
+        else:
+            raise KeyError(sender_type)
+        setattr(self.parent.draw_image.color, dest, max(min(value, 255), 0))
+        self.update_controls()
+        self.dirty = True
+
+    def set_hsv(self, value, dest, sender_type):
+        slider, spinner = (
+            (self.hue_slider, self.hue_input),
+            (self.saturation_slider, self.saturation_input),
+            (self.value_slider, self.value_input),
+        )[dest]
+        if sender_type is Slider:
+            limited_value = max(min(value, slider.extent), 0)
+            if limited_value != value:
+                value = limited_value
+                slider.value = value
+            value = value * 255 // slider.extent
+            spinner.value = int(value)
+        elif sender_type is Spinner:
+            limited_value = max(min(int(value), 255), 0)
+            if limited_value != value:
+                value = limited_value
+                spinner.value = value
+            slider.value = value * slider.extent // 255
+        else:
+            raise KeyError(sender_type)
+        self.parent.draw_image.color = self.hsv_to_color(
+            (
+                value if dest == 0 else self.hue_input.value,
+                value if dest == 1 else self.saturation_input.value,
+                value if dest == 2 else self.value_input.value,
+            )
         )
+        self.update_controls(True)
+        self.dirty = True
 
-    key_methods = {frozenset(): {pygame.K_ESCAPE: Modal.deactivate}}
+    def set_color(self, value):
+        try:
+            color = pygame.Color(value)
+        except:
+            color = pygame.Color(f"0x{value}")
+        self.parent.draw_image.color = color
+        self.update_controls()
+        self.dirty = True
index 20706de7cf542a2d526e2e69fd35b05892d6846d..7244da69fad200c6dfea614ba0bd4a19380a8e2a 100644 (file)
@@ -14,8 +14,6 @@ class DrawImage(Child):
 
     def __init__(self, parent, color):
         super().__init__(parent)
-        if not isinstance(color, pygame.Color):
-            color = pygame.Color(color)
         self.color = color
         self.line_width = 1
         self.image = pygame.Surface(self.surf.get_size(), 0, 24)
@@ -38,7 +36,7 @@ class DrawImage(Child):
         self.dirty = True
 
     def dot(self, surf, pos):
-        if self.stroke_method == self.StrokeMethod.SQUARE:
+        if self.stroke_method is self.StrokeMethod.SQUARE:
             pass
         elif self.line_width // 2 == 0:
             surf.set_at(pos, self.color)
@@ -63,18 +61,20 @@ class DrawImage(Child):
         if ev.pos == self.prev_pos:
             self.dot(self.image, ev.pos)
             return
-        if self.stroke_method == self.StrokeMethod.ROUND:
+        if self.stroke_method is self.StrokeMethod.ROUND:
             StrokeRoundLine(
                 self.prev_pos, ev.pos, self.line_width
             ).draw(self.image, self.color)
-        elif self.stroke_method == self.StrokeMethod.SQUARE:
+        elif self.stroke_method is self.StrokeMethod.SQUARE:
             StrokeSquareLine(
                 self.prev_pos, ev.pos, self.line_width
             ).draw(self.image, self.color)
-        elif self.stroke_method == self.StrokeMethod.PYGAME:
+        elif self.stroke_method is self.StrokeMethod.PYGAME:
             pygame.draw.line(
                 self.image, self.color, self.prev_pos, ev.pos, self.line_width
             )
+        else:
+            raise IndexError(self.stroke_method)
         self.prev_pos = ev.pos
 
     def handle_mousebuttonup(self, ev):
index 21536ad6b94369dadbfbc84e391a0f39d97c90f4..6982cd3a745beb610d6e48697fc4ce69903996cf 100644 (file)
@@ -1,11 +1,12 @@
 import pygame
 
-from ui import DropDown, Label, Modal, Slider, TextInput
+from ui import DropDown, Label, Slider, TextInput
 
 from .draw_image import DrawImage
+from .base_menu import BaseMenu
 
 
-class LineMenu(Modal):
+class LineMenu(BaseMenu):
     def __init__(self, parent):
         super().__init__(parent)
         size = self.surf.get_size()
@@ -75,5 +76,3 @@ class LineMenu(Modal):
         rect.inflate_ip((32, 32))
         pygame.draw.rect(self.surf, "black", rect)
         pygame.draw.rect(self.surf, "gray", rect, 1)
-
-    key_methods = {frozenset(): {pygame.K_ESCAPE: Modal.deactivate}}
index e1b08524ecb9054b37f67389804523e3cf2b6a92..5dd9f964cfeeb8b299b57939af12e23a9e7cbe48 100644 (file)
@@ -2,12 +2,13 @@ from functools import partial
 
 import pygame
 
-from ui import Button, Label, Modal
+from ui import Button, Label
 
 from .book_manager import BookManager
+from .base_menu import BaseMenu
 
 
-class PageMenu(Modal):
+class PageMenu(BaseMenu):
     def __init__(self, parent):
         super().__init__(parent)
         size = self.surf.get_size()
@@ -25,11 +26,11 @@ class PageMenu(Modal):
         )
         y += button_height + 16
         callbacks_per_value = (
-            ("|<", partial(self.parent.nav, BookManager.Nav.FIRST)),
-            ("<", partial(self.parent.nav, BookManager.Nav.PREV)),
-            (">", partial(self.parent.nav, BookManager.Nav.NEXT)),
-            (">|", partial(self.parent.nav, BookManager.Nav.LAST)),
-            ("NEW", partial(self.parent.nav, BookManager.Nav.NEW)),
+            ("|<", partial(self.nav, BookManager.Nav.FIRST)),
+            ("<", partial(self.nav, BookManager.Nav.PREV)),
+            (">", partial(self.nav, BookManager.Nav.NEXT)),
+            (">|", partial(self.nav, BookManager.Nav.LAST)),
+            ("NEW", partial(self.nav, BookManager.Nav.NEW)),
         )
         button_width = size[0] // (2 * len(callbacks_per_value))
         x = self.base_rect.left
@@ -48,12 +49,24 @@ class PageMenu(Modal):
         pygame.draw.rect(self.surf, "black", rect)
         pygame.draw.rect(self.surf, "gray", rect, 1)
 
-    key_methods = {frozenset(): {pygame.K_ESCAPE: Modal.deactivate}}
-
     def update_label(self):
         self.label.value = " / ".join(
             (
                 str(self.parent.book_manager.get_current_index() + 1),
                 str(len(self.parent.book_manager.pages)),
             )
-        )
\ No newline at end of file
+        )
+
+    def nav(self, nav):
+        self.parent.nav(nav)
+
+    key_methods = {
+        **BaseMenu.key_methods,
+        frozenset({pygame.KMOD_CTRL}): {
+            pygame.K_HOME: partial(nav, nav=BookManager.Nav.FIRST),
+            pygame.K_PAGEUP: partial(nav, nav=BookManager.Nav.PREV),
+            pygame.K_PAGEDOWN: partial(nav, nav=BookManager.Nav.NEXT),
+            pygame.K_END: partial(nav, nav=BookManager.Nav.LAST),
+            pygame.K_n: partial(nav, nav=BookManager.Nav.NEW),
+        }
+    }
index afd72485c01af97b51b2b10b4106527274b8086f..92f4a299cb0102bee4a95870473946a95bf1a82d 100755 (executable)
@@ -4,8 +4,7 @@ from functools import partial
 from math import pi, sqrt
 from time import time
 
-import pygame
-
+from launch import run, pygame
 from ui import Button, MessageBox, Rect, Root
 from vectors import StrokeCircleSegment
 
@@ -285,4 +284,4 @@ class ConnectFour(Root):
 
 
 if __name__ == "__main__":
-    ConnectFour().run()
+    run()
index 366fe36f1f0d70fc5e0d246ca99e21de03c5d066..77dc1ac0f003fda50831c5188dc4080afb038232 100755 (executable)
@@ -3,7 +3,6 @@
 script_dir="$(realpath -Pe "$(dirname "${0}")")"
 executable="${script_dir}/zenbook_conf.py"
 icon="${script_dir}/laptop-single.svg"
-virtual_env=
 
 help() {
     echo " ${0} [--executable PATH] [--icon PATH] [--virtual-env PATH] | -h|--help"
@@ -27,10 +26,6 @@ while (( $# )); do
         icon="${2}"
         shift
         ;;
-    --virtual-env)
-        virtual_env="${2}"
-        shift
-        ;;
     -h|--help)
         help=1
         ;;
@@ -49,13 +44,7 @@ fi
 
 echo "[Desktop Entry]"
 echo "Type=Application"
-if [[ "${virtual_env}" ]]; then
-    virtual_env="$(realpath -Pe "${virtual_env}")"
-    env="VIRTUAL_ENV=${virtual_env} PATH=\$VIRTUAL_ENV/bin:\$PATH "
-else
-    env=
-fi
-echo "Exec=/usr/bin/sh -c '${env}$(realpath -Pe "${executable}")'"
+echo "Exec=$(realpath -Pe "${executable}")"
 echo "Icon=$(realpath -Pe "${icon}")"
 echo ""
 echo "Name=Zenbook Duo settings"
diff --git a/launch/__init__.py b/launch/__init__.py
new file mode 100644 (file)
index 0000000..8f59c15
--- /dev/null
@@ -0,0 +1,77 @@
+import os
+import shlex
+import sys
+from contextlib import redirect_stdout
+from importlib import import_module
+from inspect import stack
+from io import StringIO
+from pathlib import Path
+from time import time
+
+
+def setup_venv():
+    venv_dir = (Path(__file__).parents[1] / "venv").absolute()
+    if venv_dir.is_dir():
+        install_venv = max(
+            p.stat().st_mtime
+            for p in (
+                venv_dir,
+                *(
+                    base / ent
+                    for base, dirs, files in venv_dir.walk()
+                    for ent in (*dirs, *files)
+                )
+            )
+        ) < time() - 86400
+    else:
+        if venv_dir.exists():
+            venv_dir.unlink()
+        assert not venv_dir.exists()
+        os.system(shlex.join((sys.executable, "-m", "venv", str(venv_dir))))
+        install_venv = True
+    new_env = {"VIRTUAL_ENV": str(venv_dir)}
+    venv_bin_str = str(venv_dir / "bin")
+    if venv_bin_str not in os.environ['PATH'].split(os.pathsep):
+        new_env["PATH"] = f"{venv_bin_str}{os.pathsep}{os.environ['PATH']}"
+    os.environ.update(new_env)
+    if install_venv:
+        os.system("python -m pip install -qU pip")
+        os.system("python -m pip install -qU pygame")
+        venv_dir.touch(0o755, True)
+    os.execl(sys.executable, Path(sys.executable).name, *sys.argv)
+    exit(1)
+
+
+try:
+    with redirect_stdout(StringIO()):
+        # ruff: noqa: F401
+        import pygame  # type: ignore
+except ImportError:
+    setup_venv()
+    raise
+
+
+def find_root(module_globals):
+    from ui import Root
+
+    for key, value in module_globals.items():
+        if key.startswith("__") and key.endswith("__"):
+            continue
+        if value is not Root and isinstance(value, type) and issubclass(value, Root):
+            return value
+    return None
+
+
+def run():
+    for fi in stack():
+        caller_name = fi.frame.f_globals["__name__"]
+        if caller_name == "__main__" or caller_name.endswith(".__main__"):
+            module_globals = fi.frame.f_globals
+            break
+    else:
+        raise ValueError("Could not determine caller name")
+    root_class = find_root(module_globals)
+    if root_class is None:
+        package = Path(module_globals["__file__"]).stem
+        root_class = find_root(import_module(f"{package}.{package}").__dict__)
+    root_class().run()
index 950bc2a7a97fa6ab49b37e0ee52c97fb6febbe3d..2771efce2393ef416fe48a78354ea8b3046b22bb 100755 (executable)
--- a/memory.py
+++ b/memory.py
@@ -4,8 +4,7 @@ from functools import partial
 from pathlib import Path
 from secrets import choice
 
-import pygame
-
+from launch import run, pygame
 from ui import Button, Child, Label, Modal, Root, TextInput
 
 
@@ -305,4 +304,4 @@ class MemoryGame(Root):
 
 
 if __name__ == "__main__":
-    MemoryGame().run()
+    run()
diff --git a/rps.py b/rps.py
index 7cf8645ac0e5cedbfd127be1d675208e28522f85..39415ea0acec3add4bef1894a8320d4e38ad0b34 100755 (executable)
--- a/rps.py
+++ b/rps.py
@@ -1,18 +1,6 @@
 #!/usr/bin/env python3
 
-import sys
-from contextlib import redirect_stdout
-from io import StringIO
-from pathlib import Path
-
-sys.path.append(str(Path(__file__).parent))
-
-from rps.rps import RockPaperScissors
-
-with redirect_stdout(StringIO()):
-    # ruff: noqa: F401
-    import pygame  # type: ignore
-
+from launch import run
 
 if __name__ == "__main__":
-    RockPaperScissors().run()
+    run()
index 2bdd8d7da14680d9b721d9a1b6c75bbc14ec0a78..bc0c0490d7af51d208d302e740984cd34055c0f4 100644 (file)
@@ -1,15 +1,14 @@
+from copy import deepcopy
 from functools import partial
 
 import pygame
 
-from .utils import CowDict
-
 
 class EventMethodDispatcher:
     MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT)
 
     def __init__(self):
-        self.key_methods = CowDict(getattr(type(self), "key_methods", {}))
+        self.key_methods = deepcopy(getattr(type(self), "key_methods", {}))
 
     def get_key_method(self, key, mod):
         mods = set()
index 7ce43e19052efcc6ab4e78b8a42db4dffa4d7198..176f624442874fc911c5b27a9b8be8b4ed1549fd 100644 (file)
@@ -4,13 +4,29 @@ import pygame
 from .child import Child
 
 
-class HorizontalSlider(Child):
-    def _get_extent_base(self):
-        return self.rect.width
+class Slider(Child):
+    class Direction(Enum):
+        HORIZONTAL = auto()
+        VERTICAL = auto()
 
-    def __init__(self, parent, rect, value=0, handle_size=None, callback=None):
+    def _get_extent_base(self):
+        return {
+            self.Direction.HORIZONTAL: self.rect.width,
+            self.Direction.VERTICAL: self.rect.height,
+        }[self.direction]
+
+    def __init__(
+        self,
+        parent,
+        rect,
+        direction,
+        value=0,
+        handle_size=None,
+        callback=None,
+    ):
         super().__init__(parent)
         self.rect = rect
+        self.direction = direction
         self.extent = (
             self._get_extent_base() - (1 if handle_size is None else handle_size)
         )
@@ -20,34 +36,60 @@ class HorizontalSlider(Child):
         self._pushed = None
 
     def value_from_pos(self, pos):
-        return pos[0]
+        return {
+            self.Direction.HORIZONTAL: pos[0],
+            self.Direction.VERTICAL: pos[1],
+        }[self.direction]
 
     def set_rel_value(self, value):
-        self.set_value(value - self.rect.left)
+        self.set_value(
+            {
+                self.Direction.HORIZONTAL: value - self.rect.left,
+                self.Direction.VERTICAL: self.extent - (value - self.rect.top),
+            }[self.direction]
+        )
 
     def get_cursor_rect(self):
         value, limited = self.map_value()
-        return pygame.Rect(
-            (self.rect.left + value, self.rect.top),
-            (self.handle_size, self.rect.height),
-        ), limited
+        if self.direction is self.Direction.HORIZONTAL:
+            rect = pygame.Rect(
+                (self.rect.left + value, self.rect.top),
+                (self.handle_size, self.rect.height),
+            )
+        elif self.direction is self.Direction.VERTICAL:
+            rect = pygame.Rect(
+                (self.rect.left, self.rect.top + self.extent - value),
+                (self.rect.width, self.handle_size),
+            )
+        else:
+            raise IndexError(self.direction)
+        return rect, limited
 
     def draw_cursor_line(self):
         value, limited = self.map_value()
-        x = self.rect.left + value
-        pygame.draw.line(
-            self.surf,
-            "dimgray" if limited else "gray",
-            (x, self.rect.top),
-            (x, self.rect.bottom),
-            8,
-        )
+        if self.direction is self.Direction.HORIZONTAL:
+            x = self.rect.left + value
+            points = (x, self.rect.top), (x, self.rect.bottom)
+        elif self.direction is self.Direction.VERTICAL:
+            y = self.rect.top + self.extent - value
+            points = (self.rect.left, y), (self.rect.right, y)
+        else:
+            raise IndexError(self.direction)
+        pygame.draw.line(self.surf, "dimgray" if limited else "gray", *points, 8)
 
     def page_flip(self, cursor_rect, value):
-        if value < cursor_rect.left:
-            return max(self.value - cursor_rect.width, 0)
+        if self.direction is self.Direction.HORIZONTAL:
+            if value < cursor_rect.left:
+                return max(self.value - cursor_rect.width, 0)
+            else:
+                return min(self.value + cursor_rect.width, self.extent)
+        elif self.direction is self.Direction.VERTICAL:
+            if value < cursor_rect.top:
+                return min(self.value + cursor_rect.height, self.extent)
+            else:
+                return max(self.value - cursor_rect.height, 0)
         else:
-            return min(self.value + cursor_rect.width, self.extent)
+            raise IndexError(self.direction)
 
     def set_value(self, value):
         if value == self.value:
@@ -84,7 +126,11 @@ class HorizontalSlider(Child):
         self.pushed = value
 
     def get_rel(self, value):
-        return self.pushed - value
+        if self.direction is self.Direction.HORIZONTAL:
+            return self.pushed - value
+        elif self.direction is self.Direction.VERTICAL:
+            return value - self.pushed
+        raise IndexError(self.direction)
 
     def handle_mousemotion(self, ev):
         if self.pushed is None:
@@ -125,7 +171,7 @@ class HorizontalSlider(Child):
             value = -value
             limited = True
         if value > self.extent:
-            value = int(self.extent * (self.extent / value))
+            value = self.extent ** 2 // value
             limited = True
         return value, limited
 
@@ -138,54 +184,3 @@ class HorizontalSlider(Child):
         else:
             color = "gray"
         pygame.draw.rect(self.surf, color, rect)
-
-
-class VerticalSlider(HorizontalSlider):
-    def _get_extent_base(self):
-        return self.rect.height
-
-    def value_from_pos(self, pos):
-        return pos[1]
-
-    def set_rel_value(self, value):
-        self.set_value(self.extent - (value - self.rect.top))
-
-    def get_cursor_rect(self):
-        value, limited = self.map_value()
-        return pygame.Rect(
-            (self.rect.left, self.rect.top + self.extent - value),
-            (self.rect.width, self.handle_size),
-        ), limited
-
-    def draw_cursor_line(self):
-        value, limited = self.map_value()
-        y = self.rect.top + self.extent - value
-        pygame.draw.line(
-            self.surf,
-            "dimgray" if limited else "gray",
-            (self.rect.left, y),
-            (self.rect.right, y),
-            8,
-        )
-
-    def page_flip(self, cursor_rect, value):
-        if value < cursor_rect.top:
-            return min(self.value + cursor_rect.height, self.extent)
-        else:
-            return max(self.value - cursor_rect.height, 0)
-
-    def get_rel(self, value):
-        return value - self.pushed
-
-
-class Slider:
-    class Direction(Enum):
-        HORIZONTAL = auto()
-        VERTICAL = auto()
-
-    def __new__(cls, parent, rect, direction, value=0, handle_size=None, callback=None):
-        if direction == cls.Direction.HORIZONTAL:
-            klass = HorizontalSlider
-        else:  # direction == cls.Direction.VERTICAL
-            klass = VerticalSlider
-        return klass(parent, rect, value, handle_size, callback)
index 40b0b21ab6fc477bf9283f3d9cc0f1cc228baac7..43547df00228d7b4dfae7418921f676486e4968c 100644 (file)
@@ -81,6 +81,7 @@ class Spinner(Parent, Child):
     def __init__(self, parent, rect, callback, value=0):
         super().__init__(parent)
         self.callback = callback
+        self._value = 0
         self.value = value
         button_size = (rect.height // 2, rect.height // 2)
         self.text_input = TextInput(
@@ -111,38 +112,51 @@ class Spinner(Parent, Child):
             partial(self.spin_callback, -1),
         )
 
-    def call_callback(self, value):
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
         try:
             int_value = int(value)
         except ValueError:
-            pass
-        else:
-            if int_value != self.value:
-                self.value = int_value
-                self.callback(int_value)
+            return
+        if int_value == self.value:
+            return
+        self.value = int_value
         str_value = str(self.value)
         if str_value != self.text_input.value:
-            self.text_input.value = str(self.value)
+            self.text_input.value = str_value
             self.dirty = True
 
+    def call_callback(self, value):
+        old_value = self.value
+        self.value = value
+        if old_value != self.value:
+            self.callback(self.value)
+
     def spin_callback(self, value):
         self.value += value
-        self.text_input.value = str(self.value)
         self.callback(self.value)
         self.dirty = True
 
 
 class FloatSpinner(Spinner):
-    def call_callback(self, value):
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
         try:
             float_value = float(value)
         except ValueError:
-            pass
-        else:
-            if float_value != self.value:
-                self.value = float_value
-                self.callback(float_value)
+            return
+        if float_value == self.value:
+            return
+        self.value = float_value
         str_value = str(self.value)
         if str_value != self.text_input.value:
-            self.text_input.value = str(self.value)
+            self.text_input.value = str_value
             self.dirty = True
diff --git a/ui/utils.py b/ui/utils.py
deleted file mode 100644 (file)
index a0f0742..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-class CowDict:
-    def __init__(self, orig_dict):
-        self.orig_dict = orig_dict
-        self.copy = None
-
-    @property
-    def dict(self):
-        return self.copy or self.orig_dict
-
-    def __getitem__(self, key):
-        return self.dict[key]
-
-    def _clone(self):
-        if self.copy is None:
-            self.copy = self.orig_dict.copy()
-
-    def __setitem__(self, key, value):
-        self._clone()
-        self.copy[key] = value
-
-    def __getattr__(self, item):
-        mod_methods = ("clear", "pop", "popitem", "setdefault", "update")
-        if self.copy is None and item in mod_methods:
-            self._clone()
-        return getattr(self.dict, item)
index 42ba26ff974cc1cf71bbde0b8850cea818923782..8f88a100e653819dbcdacec3be400c70606904d6 100755 (executable)
@@ -4,8 +4,7 @@ from pathlib import Path
 from secrets import choice, randbelow
 from time import time
 
-import pygame
-
+from launch import run, pygame
 from ui import Button, Child, FloatSpinner, Label, Root, Spinner
 
 
@@ -263,4 +262,4 @@ class VSMemory(Root):
 
 
 if __name__ == "__main__":
-    VSMemory().run()
+    run()
index 0ed472d51db16c3d2e3b6903802393a9c30fad05..39415ea0acec3add4bef1894a8320d4e38ad0b34 100755 (executable)
@@ -1,16 +1,6 @@
 #!/usr/bin/env python3
 
-import sys
-from contextlib import redirect_stdout
-from io import StringIO
-from pathlib import Path
+from launch import run
 
-sys.path.append(str(Path(__file__).parent))
-
-from zenbook_conf.zenbook_conf import ZenbookConf
-
-with redirect_stdout(StringIO()):
-    # ruff: noqa: F401
-    import pygame  # type: ignore
-
-ZenbookConf().run()
+if __name__ == "__main__":
+    run()