From 8a56b064da0f3953b670a31c8ea2e3d3e35cfada Mon Sep 17 00:00:00 2001 From: mar77i Date: Mon, 10 Feb 2025 02:55:42 +0100 Subject: [PATCH] scroll field, but without scrollbars --- bookpaint/draw_ui.py | 10 ++++----- ui/__init__.py | 1 + ui/base.py | 52 ++++++++++++++++++++++++++++++-------------- ui/button.py | 4 ++-- ui/drop_down.py | 12 +++++----- ui/fps_widget.py | 7 +++--- ui/icon.py | 4 ++-- ui/icon_button.py | 4 ++-- ui/label.py | 4 ++-- ui/message_box.py | 8 +++---- ui/modal.py | 6 ++--- ui/scroll.py | 45 ++++++++++++++++++++++++++++++++++++++ ui/slider.py | 4 ++-- ui/spinner.py | 19 ++++++++-------- ui/switch.py | 4 ++-- ui/tab_bar.py | 20 ++++++++--------- ui/text_input.py | 8 ++++--- 17 files changed, 139 insertions(+), 73 deletions(-) create mode 100644 ui/scroll.py diff --git a/bookpaint/draw_ui.py b/bookpaint/draw_ui.py index eb19e12..6dd724d 100644 --- a/bookpaint/draw_ui.py +++ b/bookpaint/draw_ui.py @@ -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): diff --git a/ui/__init__.py b/ui/__init__.py index c07599a..affe335 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -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 diff --git a/ui/base.py b/ui/base.py index 6fe79da..fc86575 100644 --- a/ui/base.py +++ b/ui/base.py @@ -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) diff --git a/ui/button.py b/ui/button.py index 9c2c14a..3d6286f 100644 --- a/ui/button.py +++ b/ui/button.py @@ -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 diff --git a/ui/drop_down.py b/ui/drop_down.py index d644679..b09ec68 100644 --- a/ui/drop_down.py +++ b/ui/drop_down.py @@ -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) diff --git a/ui/fps_widget.py b/ui/fps_widget.py index 472efa8..d14866b 100644 --- a/ui/fps_widget.py +++ b/ui/fps_widget.py @@ -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 diff --git a/ui/icon.py b/ui/icon.py index 35ec633..9e3becb 100644 --- a/ui/icon.py +++ b/ui/icon.py @@ -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): diff --git a/ui/icon_button.py b/ui/icon_button.py index ebb077a..9b952a6 100644 --- a/ui/icon_button.py +++ b/ui/icon_button.py @@ -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): diff --git a/ui/label.py b/ui/label.py index d8cd65c..20de04c 100644 --- a/ui/label.py +++ b/ui/label.py @@ -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 diff --git a/ui/message_box.py b/ui/message_box.py index f95c346..3c43152 100644 --- a/ui/message_box.py +++ b/ui/message_box.py @@ -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), diff --git a/ui/modal.py b/ui/modal.py index ef7bba4..0a1b4ef 100644 --- a/ui/modal.py +++ b/ui/modal.py @@ -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 index 0000000..22c7614 --- /dev/null +++ b/ui/scroll.py @@ -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) diff --git a/ui/slider.py b/ui/slider.py index aa516d1..5f1b510 100644 --- a/ui/slider.py +++ b/ui/slider.py @@ -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] diff --git a/ui/spinner.py b/ui/spinner.py index 066af4b..9574001 100644 --- a/ui/spinner.py +++ b/ui/spinner.py @@ -5,7 +5,7 @@ from time import time import pygame -from .base import Child, Parent +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, diff --git a/ui/switch.py b/ui/switch.py index 69e5d6d..634ca68 100644 --- a/ui/switch.py +++ b/ui/switch.py @@ -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): diff --git a/ui/tab_bar.py b/ui/tab_bar.py index 8fafa30..180785c 100644 --- a/ui/tab_bar.py +++ b/ui/tab_bar.py @@ -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 diff --git a/ui/text_input.py b/ui/text_input.py index 4e3a08a..6481b51 100644 --- a/ui/text_input.py +++ b/ui/text_input.py @@ -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 -- 2.51.0