From: mar77i Date: Mon, 20 Jan 2025 16:08:50 +0000 (+0100) Subject: add switch X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=74ab27975ee86e70b1b24709463fb75c01db1f45;p=zenbook_gui add switch --- diff --git a/ui/ui.py b/ui/ui.py index 024c705..8f9e6df 100644 --- 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