]> git.mar77i.info Git - zenbook_gui/commitdiff
long-time update: restructuring color_menu, not reparent TabBar and better abstract...
authormar77i <mar77i@protonmail.ch>
Wed, 2 Jul 2025 15:03:30 +0000 (17:03 +0200)
committermar77i <mar77i@protonmail.ch>
Wed, 2 Jul 2025 15:03:30 +0000 (17:03 +0200)
bookpaint/book_manager.py
bookpaint/bookpaint.py
bookpaint/color_menu.py
memory.py
ui/spinner.py
ui/tab_bar.py

index dbe0ec3cab1de648ed41b6e06a046c92b7159a16..6f7e275a1f3ae19ae8ee110fed89cbbcd9502a42 100644 (file)
@@ -46,7 +46,7 @@ class BookManager:
                 self.current_file = self.get_new_file()
             self.pages.append(self.current_file)
             self.free_page_no += 1
-        pygame.image.save(self.parent.draw_image.surface, self.current_file)
+        pygame.image.save(self.parent.draw_image.image, self.current_file)
 
     class Nav(Enum):
         FIRST = auto()
index 002531d4a7478c5ad6eb11c008bb68a065fa326c..1912efd569311d4cadf92d07cff96496f044becf 100644 (file)
@@ -31,7 +31,7 @@ class BookPaint(Root):
         self.draw_image = DrawImage(self, pygame.Color("white"))
         self.color_menu = ColorMenu(self)
         self.line_menu = LineMenu(self)
-        self.book_manager = BookManager(self, Path("book"))
+        self.book_manager = BookManager(self, Path(__file__).parents[1] / "book")
         self.page_menu = PageMenu(self)
         self.menu = Menu(self)
         self.page_menu.update_label()
index 992ce637bcfe8f5f4b72b5925bd1145006666284..b585f6c7d328e72284f89b32875d6433a6d4be1e 100644 (file)
@@ -1,9 +1,10 @@
 from colorsys import hsv_to_rgb, rgb_to_hsv
+from math import atan2, cos, pi, sin, sqrt, tau
 from functools import partial
 
 import pygame
 
-from ui import Child, Label, Slider, Spinner, TextInput
+from ui import Child, Label, Slider, Spinner, TabBar, TextInput
 
 from .base_menu import BaseMenu
 
@@ -18,7 +19,243 @@ class ColorLabel(Child):
         pygame.draw.rect(self.surf, self.color, self.rect)
 
 
+def draw_cursor(surf, color, pos):
+    r = 16
+    pygame.draw.polygon(
+        surf,
+        color,
+        (
+            (pos[0], pos[1] - r),
+            (pos[0] + r, pos[1]),
+            (pos[0], pos[1] + r),
+            (pos[0] - r, pos[1]),
+        )
+    )
+
+
+class ColorCircle(Child):
+    def render_hue_circle(self):
+        surf = pygame.Surface(self.rect.size, 0, 24)
+        center = tuple(a // 2 for a in surf.get_size())
+        rs = self.radii_squared
+        with pygame.PixelArray(surf) as pa:
+            for x, col in enumerate(pa):
+                for y in range(len(col)):
+                    rel = (x - center[0], y - center[1])
+                    if rs[0] <= rel[0] ** 2 + rel[1] ** 2 <= rs[1]:
+                        angle = atan2(-rel[1], rel[0])
+                        if angle < 0:
+                            angle += tau
+                        pa[x][y] = ColorMenu.hsv_to_color(
+                            (int(angle * 255 / tau), 255, 255)
+                        )
+        return surf
+
+    def get_overlay_surfs(self):
+        size = self.hue_surf.get_size()
+        overlay_surfs = (
+            pygame.Surface(size, pygame.SRCALPHA, 32),
+            pygame.Surface(size, pygame.SRCALPHA, 32),
+        )
+        for i, surf in enumerate(overlay_surfs):
+            with pygame.PixelArray(surf) as pa:
+                for x, col in enumerate(pa):
+                    for y in range(len(col)):
+                        pa[x][y] = pygame.Color(
+                            y // 2, y // 2, y // 2, 255 - (y, x)[i] * 255 // max(size),
+                        )
+        return overlay_surfs
+
+    def __init__(self, parent, rect):
+        super().__init__(parent)
+        self.rect = rect
+        self.radii = (378, min(rect.size) // 2)
+        self.radii_squared = tuple(r ** 2 for r in self.radii)
+        self.avg_radius = sum(self.radii) / len(self.radii)
+        self.hue_circle = self.render_hue_circle()
+        self.hue_angle = 0
+        sz = (512, 512)
+        self.hue_surf = pygame.Surface(sz, pygame.SRCALPHA, 32)
+        self.hue_surf_rect = pygame.Rect(
+            (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2), sz
+        )
+        self.sv_pos = (0, 0)
+        self.overlay_surfs = self.get_overlay_surfs()
+        self.pressed = None
+
+    def get_hue_surf_angle(self):
+        angle = self.hue_angle + tau / 8
+        if angle >= tau:
+            angle -= tau
+        return angle
+
+    def draw(self):
+        self.surf.blit(self.hue_circle, self.rect.topleft)
+        draw_cursor(
+            self.surf,
+            "black",
+            (
+                self.rect.centerx + cos(self.hue_angle) * self.avg_radius,
+                self.rect.centery - sin(self.hue_angle) * self.avg_radius,
+            ),
+        )
+        color = ColorMenu.hsv_to_color((self.hue_angle * 255 / tau, 255, 255))
+        self.hue_surf.fill(color)
+        self.hue_surf.blit(self.overlay_surfs[0], (0, 0))
+        self.hue_surf.blit(self.overlay_surfs[1], (0, 0))
+        angle = self.get_hue_surf_angle()
+        rotated_hue_surf = pygame.transform.rotate(self.hue_surf, angle * 360 / tau)
+        sz = rotated_hue_surf.get_size()
+        self.surf.blit(
+            rotated_hue_surf,
+            (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2)
+        )
+
+        sz = self.hue_surf_rect.size
+        sv_rel = (self.sv_pos[0] - sz[0] // 2, self.sv_pos[1] - sz[1] // 2)
+        rel_dist = sqrt(sv_rel[0] ** 2 + sv_rel[1] ** 2)
+        rel_angle = atan2(sv_rel[1], sv_rel[0]) - angle
+        flipped_hue_angle = self.hue_angle - pi / 2
+        if flipped_hue_angle < 0:
+            flipped_hue_angle += tau
+        draw_cursor(
+            self.surf,
+            ColorMenu.hsv_to_color((flipped_hue_angle * 255 / tau, 255, 255)),
+            (
+                self.rect.centerx + cos(rel_angle) * rel_dist,
+                self.rect.centery + sin(rel_angle) * rel_dist,
+            ),
+        )
+
+    def handle_mousebuttondown(self, ev):
+        if ev.button != 1 or not self.rect.collidepoint(ev.pos):
+            return
+        rel = (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+        rs = self.radii_squared
+        sq = rel[0] ** 2 + rel[1] ** 2
+        if rs[0] <= sq <= rs[1]:
+            self.pressed = 1
+            self.set_rel_hue(rel)
+        elif rs[0] > sq:
+            self.pressed = 2
+            self.set_rel_sv(rel, sq)
+
+    def handle_mousemotion(self, ev):
+        if self.pressed == 1:
+            self.set_rel_hue(
+                (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+            )
+        elif self.pressed == 2:
+            self.set_rel_sv(
+                (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+            )
+
+    def handle_mousebuttonup(self, ev):
+        if self.pressed == 1:
+            self.set_rel_hue(
+                (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+            )
+            self.pressed = None
+        elif self.pressed == 2:
+            self.set_rel_sv(
+                (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery)
+            )
+            self.pressed = None
+
+    def set_rel_hue(self, rel):
+        angle = atan2(-rel[1], rel[0])
+        if angle < 0:
+            angle += tau
+        self.hue_angle = angle
+        self.parent.set_channel(angle * 255 / tau, "h", self)
+
+    def set_rel_sv(self, rel, sq=None):
+        rel_dist = sqrt(sq if sq is not None else rel[0] ** 2 + rel[1] ** 2)
+        rel_angle = atan2(rel[1], rel[0]) + self.get_hue_surf_angle()
+        sz = self.hue_surf_rect.size
+        x, y = (
+            cos(rel_angle) * rel_dist + sz[0] // 2,
+            sin(rel_angle) * rel_dist + sz[1] // 2,
+        )
+        if x < 0:
+            x = 0
+        elif x > sz[0]:
+            x = sz[0]
+        if y < 0:
+            y = 0
+        elif y > sz[1]:
+            y = sz[1]
+        self.sv_pos = (x, y)
+        self.parent.set_channel(x * 255 / sz[0], "s", self)
+        self.parent.set_channel(y * 255 / sz[1], "v", self)
+
+
+class HSMap(Child):
+    @staticmethod
+    def get_hs_surf(size):
+        surf = pygame.Surface(size, 0, 24)
+        with pygame.PixelArray(surf) as pa:
+            for x, col in enumerate(pa):
+                for y in range(len(col)):
+                    col[y] = pygame.Color(
+                        *(
+                            int(a * 255)
+                            for a in hsv_to_rgb(x / size[0], 1 - y / (size[1] - 1), 1.0)
+                        )
+                    )
+        return surf
+
+    def __init__(self, parent, rect):
+        super().__init__(parent)
+        self.rect = rect
+        self.hs_surf = self.get_hs_surf(rect.size)
+        self.hs_pos = (0, 0)
+
+    def draw(self):
+        pygame.draw.rect(self.surf, "green", self.rect, 1)
+        self.surf.blit(self.hs_surf, self.rect.topleft)
+        color = ColorMenu.hsv_to_color(
+            (self.hs_pos[0] * 255 / self.rect.width, 255, 255)
+        )
+        draw_cursor(
+            self.surf,
+            color,
+            (self.rect.left + self.hs_pos[0], self.rect.bottom - (1 + self.hs_pos[1])),
+        )
+
+    def set_hsv(self, value, dest):
+        self.parent.set_hsv(value, dest, type(self))
+
+
+class HSSlider(Slider):
+    def draw_cursor(self):
+        draw_cursor(self.surf, "blue", self.get_cursor_rect()[0].center)
+
+
 class ColorMenu(BaseMenu):
+    def prev_tab(self):
+        if self.tab_bar.current_tab > 0:
+            self.tab_bar.click(self.tab_bar.current_tab - 1)
+
+    def next_tab(self):
+        if self.tab_bar.current_tab < len(self.tab_bar.buttons) - 1:
+            self.tab_bar.click(self.tab_bar.current_tab + 1)
+
+    key_methods = {
+        frozenset(): {
+            pygame.K_LEFT: prev_tab,
+            pygame.K_RIGHT: next_tab,
+            **BaseMenu.key_methods[frozenset()],
+        }
+    }
+
+    def add_control(self, ui_type, *args, **kwargs):
+        dest = kwargs.pop("dest")
+        instance = ui_type(self, *args, None, **kwargs)
+        instance.callback = partial(self.set_channel, dest=dest, sender=instance)
+        self.channel_slots[dest].append(instance)
+        return instance
+
     def __init__(self, parent):
         super().__init__(parent)
         size = self.surf.get_size()
@@ -46,127 +283,211 @@ class ColorMenu(BaseMenu):
         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,
+        y += button_height + 16
+        tabbar_y = y
+        y += (button_height + 16) * 2
+
+        self.channel_slots = {"r": [], "g": [], "b": [], "h": [], "s": [], "v": []}
+        red_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_left, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_rgb, dest="r", sender_type=Slider),
+            dest="r",
         )
-        self.red_input = Spinner(
-            self,
+        red_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_left, y), (input_width, button_height)),
-            partial(self.set_rgb, dest="r", sender_type=Spinner),
+            dest="r",
         )
-        Label(
-            self,
-            pygame.Rect((label_right, y), (label_width, button_height)),
-            "H",
-            Label.HAlign.CENTER,
-        )
-        self.hue_slider = Slider(
-            self,
+        hue_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_right, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_hsv, dest=0, sender_type=Slider),
+            dest="h",
         )
-        self.hue_input = Spinner(
-            self,
+        hue_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_right, y), (input_width, button_height)),
-            partial(self.set_hsv, dest=0, sender_type=Spinner),
+            dest="h",
         )
-
         y += button_height + 16
-        Label(
-            self,
-            pygame.Rect((label_left, y), (label_width, button_height)),
-            "G",
-            Label.HAlign.CENTER,
-        )
-        self.green_slider = Slider(
-            self,
+        green_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_left, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_rgb, dest="g", sender_type=Slider),
+            dest="g",
         )
-        self.green_input = Spinner(
-            self,
+        green_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_left, y), (input_width, button_height)),
-            partial(self.set_rgb, dest="g", sender_type=Spinner),
+            dest="g",
         )
-        Label(
-            self,
-            pygame.Rect((label_right, y), (label_width, button_height)),
-            "S",
-            Label.HAlign.CENTER,
-        )
-        self.saturation_slider = Slider(
-            self,
+        saturation_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_right, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_hsv, dest=1, sender_type=Slider),
+            dest="s",
         )
-        self.saturation_input = Spinner(
-            self,
+        saturation_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_right, y), (input_width, button_height)),
-            partial(self.set_hsv, dest=1, sender_type=Spinner),
+            dest="s",
         )
-
         y += button_height + 16
-        Label(
-            self,
-            pygame.Rect((label_left, y), (label_width, button_height)),
-            "B",
-            Label.HAlign.CENTER,
-        )
-        self.blue_slider = Slider(
-            self,
+        blue_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_left, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_rgb, dest="b", sender_type=Slider),
+            dest="b",
         )
-        self.blue_input = Spinner(
-            self,
+        blue_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_left, y), (input_width, button_height)),
-            partial(self.set_rgb, dest="b", sender_type=Spinner),
+            dest="b",
         )
-        Label(
-            self,
-            pygame.Rect((label_right, y), (label_width, button_height)),
-            "V",
-            Label.HAlign.CENTER,
-        )
-        self.value_slider = Slider(
-            self,
+        value_slider = self.add_control(
+            Slider,
             pygame.Rect((slider_right, y), (slider_width, button_height)),
             Slider.Direction.HORIZONTAL,
             0,
             button_height,
-            partial(self.set_hsv, dest=2, sender_type=Slider),
+            dest="v",
         )
-        self.value_input = Spinner(
-            self,
+        value_spinner = self.add_control(
+            Spinner,
             pygame.Rect((input_right, y), (input_width, button_height)),
-            partial(self.set_hsv, dest=2, sender_type=Spinner),
+            dest="v",
+        )
+        self.color_circle = ColorCircle(
+            self,
+            pygame.Rect(
+                (self.modal_rect.centerx - 450, tabbar_y + button_height + 16),
+                (900, 900),
+            ),
+        )
+        hs_map_width = 1336
+        hs_slider_width = 64
+        self.hs_map = HSMap(
+            self,
+            pygame.Rect(
+                (
+                    self.modal_rect.centerx - hs_map_width // 2,
+                    tabbar_y + button_height + 16,
+                ),
+                (hs_map_width - hs_slider_width - 16, 900),
+            ),
+        )
+        value_slider2 = self.add_control(
+            HSSlider,
+            pygame.Rect(
+                (
+                    self.modal_rect.centerx + hs_map_width // 2 - hs_slider_width,
+                    tabbar_y + button_height + 16,
+                ),
+                (hs_slider_width, 900),
+            ),
+            Slider.Direction.VERTICAL,
+            0,
+            button_height // 2,
+            dest="v",
         )
 
-        y += button_height * 2
+        self.tab_bar = TabBar(
+            self,
+            pygame.Rect(
+                (self.modal_rect.left, tabbar_y),
+                (self.modal_rect.width, button_height),
+            ),
+            (
+                "Sliders",
+                "Hue Circle",
+                "Hue-Saturation Map",
+            ),
+            (
+                (
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_left, red_slider.rect.top),
+                            (label_width, button_height),
+                        ),
+                        "R",
+                        Label.HAlign.CENTER,
+                    ),
+                    red_slider,
+                    red_spinner,
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_right, hue_slider.rect.top),
+                            (label_width, button_height)
+                        ),
+                        "H",
+                        Label.HAlign.CENTER,
+                    ),
+                    hue_slider,
+                    hue_spinner,
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_left, green_slider.rect.top),
+                            (label_width, button_height),
+                        ),
+                        "G",
+                        Label.HAlign.CENTER,
+                    ),
+                    green_slider,
+                    green_spinner,
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_right, saturation_slider.rect.top),
+                            (label_width, button_height),
+                        ),
+                        "S",
+                        Label.HAlign.CENTER,
+                    ),
+                    saturation_slider,
+                    saturation_spinner,
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_left, blue_slider.rect.top),
+                            (label_width, button_height),
+                        ),
+                        "B",
+                        Label.HAlign.CENTER,
+                    ),
+                    blue_slider,
+                    blue_spinner,
+                    Label(
+                        self,
+                        pygame.Rect(
+                            (label_right, value_slider.rect.top),
+                            (label_width, button_height),
+                        ),
+                        "V",
+                        Label.HAlign.CENTER,
+                    ),
+                    value_slider,
+                    value_spinner,
+                ),
+                (self.color_circle,),
+                (self.hs_map, value_slider2,),
+            ),
+        )
+
+        y = tabbar_y + button_height + 16 + 900 + 16
         half_width = self.modal_rect.width // 2
         self.hex_input = TextInput(
             self,
@@ -202,26 +523,44 @@ class ColorMenu(BaseMenu):
             *(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
-        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:
+    def update_controls(self, color=None, hsv=None, sender=None):
+        if hsv is None:
+            if color is None:
+                color = self.parent.draw_image.color
+            else:
+                self.parent.draw_image.color = color
             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
+        else:
+            assert color is None
+            color = self.hsv_to_color(hsv)
+            self.parent.draw_image.color = color
+        self.hex_input.value = f"{int(color) >> 8:06x}"
+        self.color_label.color = color
+        assert tuple(color)[:3] == (color.r, color.g, color.b)
+        values = {
+            "r": color.r,
+            "g": color.g,
+            "b": color.b,
+            "h": hsv[0],
+            "s": hsv[1],
+            "v": hsv[2],
+        }
+        for key, controls in self.channel_slots.items():
+            value = values[key]
+            for control in controls:
+                if control is sender:
+                    continue
+                control.value = (
+                    value
+                    if not isinstance(control, Slider)
+                    else value * control.extent // 255
+                )
+        if sender != self.color_circle:
+            self.color_circle.hue_angle = values["h"] * tau / 255
+            sz = self.color_circle.hue_surf_rect.size
+            self.color_circle.sv_pos = (values["s"] * sz[0] / 255, values["v"] * sz[1] / 255)
+        if sender != self.hs_map:
+            ...
 
     def draw_modal(self):
         super().draw_modal()
@@ -230,58 +569,50 @@ class ColorMenu(BaseMenu):
         pygame.draw.rect(self.surf, "black", rect)
         pygame.draw.rect(self.surf, "gray", rect, 1)
 
-    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
+    RGB_LETTERS = ("r", "g", "b")
+    HSV_LETTERS = ("h", "s", "v")
 
-    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
+    def set_channel(self, value, dest, sender):
+        if isinstance(sender, Slider):
+            value = max(min(value, sender.extent), 0)
+            if getattr(sender, "value", value) != value:
+                sender.value = value
+            value = value * 255 // sender.extent
         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)
+            value = max(min(value, 255), 0)
+            if getattr(sender, "value", value) != value:
+                sender.value = value
+        kwargs = {}
+        if dest in self.RGB_LETTERS:
+            color = pygame.Color(self.parent.draw_image.color)
+            if getattr(color, dest) == value:
+                return
+            setattr(color, dest, value)
+            kwargs["color"] = color
+        elif dest in self.HSV_LETTERS:
+            hsv_controls = [
+                next(
+                    d for d in self.channel_slots[c] if not isinstance(d, Slider)
+                )
+                for c in self.HSV_LETTERS
+            ]
+            index = self.HSV_LETTERS.index(dest)
+            kwargs["hsv"] = [d.value for d in hsv_controls]
+            if sender != hsv_controls[index]:
+                if kwargs["hsv"][index] == value:
+                    return
+                kwargs["hsv"][index] = value
+        self.update_controls(sender=sender, **kwargs)
         self.dirty = True
 
-    def set_color(self, value):
-        try:
-            color = pygame.Color(value)
-        except:
-            color = pygame.Color(f"0x{value}")
+    def set_color(self, color_like):
+        if isinstance(color_like, pygame.Color):
+            color = color_like
+        else:
+            try:
+                color = pygame.Color(color_like)
+            except:
+                color = pygame.Color(f"0x{color_like}")
         self.parent.draw_image.color = color
         self.update_controls()
         self.dirty = True
index 2771efce2393ef416fe48a78354ea8b3046b22bb..129c98fa8f128ea012c507cf8a37963a604fb8dd 100755 (executable)
--- a/memory.py
+++ b/memory.py
@@ -9,8 +9,8 @@ from ui import Button, Child, Label, Modal, Root, TextInput
 
 
 class QuittableModal(Modal):
-    def handle_quit(self, ev):
-        self.parent.handle_quit(ev)
+    def handle_quit(self, _=None):
+        self.parent.handle_quit()
 
     key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}}
 
index 43547df00228d7b4dfae7418921f676486e4968c..726b254a45cce8284223310b4e7f676cea62d073 100644 (file)
@@ -82,7 +82,6 @@ class Spinner(Parent, Child):
         super().__init__(parent)
         self.callback = callback
         self._value = 0
-        self.value = value
         button_size = (rect.height // 2, rect.height // 2)
         self.text_input = TextInput(
             self,
@@ -93,6 +92,7 @@ class Spinner(Parent, Child):
             str(value),
             re.compile(r"[-+]?\d*").fullmatch,
         )
+        self.value = value
         RepeatButtonRotate(
             self,
             pygame.Rect(
@@ -124,8 +124,8 @@ class Spinner(Parent, Child):
             return
         if int_value == self.value:
             return
-        self.value = int_value
-        str_value = str(self.value)
+        self._value = int_value
+        str_value = str(int_value)
         if str_value != self.text_input.value:
             self.text_input.value = str_value
             self.dirty = True
@@ -156,7 +156,7 @@ class FloatSpinner(Spinner):
         if float_value == self.value:
             return
         self.value = float_value
-        str_value = str(self.value)
+        str_value = str(float_value)
         if str_value != self.text_input.value:
             self.text_input.value = str_value
             self.dirty = True
index 8ee76fcdd03d10c8cc8fbfcdfee3182ce8058ef3..da3c32860212bd3bd2f8169fd32f7969029012df 100644 (file)
@@ -8,7 +8,7 @@ from .parent import Parent
 
 
 class TabBar(Parent, Child):
-    def __init__(self, parent, rect, labels, groups, current_tab):
+    def __init__(self, parent, rect, labels, groups, current_tab=0):
         super().__init__(parent)
         self.labels = labels
         self.groups = groups
@@ -22,32 +22,28 @@ class TabBar(Parent, Child):
                     (rect.width // num_labels, rect.height),
                 ),
                 labels[i],
-                partial(self.update_children, i),
+                partial(self.click, i),
                 False,
             )
             for i in range(num_labels)
         ]
         self.update_children()
 
-    def update_children(self, i=None):
-        if i is not None:
-            if self.current_tab == i:
-                return
-            self.current_tab = i
+    def click(self, i):
+        if self.current_tab == i:
+            return
+        self.current_tab = i
+        self.update_children()
+
+    def update_children(self):
         for i, (button, group) in enumerate(zip(self.buttons, self.groups)):
             is_current_tab = i == self.current_tab
             if button.highlight != is_current_tab:
                 button.highlight = is_current_tab
                 self.dirty = True
             for item in group:
-                if item.parent is not self:
-                    item.parent = self
-                is_child_enabled = item.enabled
-                if is_current_tab == is_child_enabled:
+                if is_current_tab == item.enabled:
                     continue
-                if is_current_tab:
-                    item.enabled = True
-                elif is_child_enabled:
-                    item.enabled = False
+                item.enabled = is_current_tab
                 self.dirty = True
         assert len(self.children) == len(set(self.children))