From 31d10697091433951ae2acd086a859884fd8c45a Mon Sep 17 00:00:00 2001 From: Faraphel Date: Mon, 13 Feb 2023 20:32:23 +0100 Subject: [PATCH] added Button widget (label to fix) --- main.pyw | 39 ++++++++++- source/gui/sprite/Sprite.py | 24 +++++++ source/gui/sprite/__init__.py | 1 + source/gui/widget/Button.py | 102 +++++++++++++++++++++++++++++ source/gui/widget/FPSDisplay.py | 6 +- source/gui/widget/Text.py | 6 +- source/gui/widget/__init__.py | 1 + source/gui/widget/abc/BoxWidget.py | 32 +++++++-- source/utils/__init__.py | 1 + source/utils/dict.py | 33 ++++++++++ 10 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 source/gui/sprite/Sprite.py create mode 100644 source/gui/sprite/__init__.py create mode 100644 source/gui/widget/Button.py create mode 100644 source/utils/dict.py diff --git a/main.pyw b/main.pyw index facc814..c009e07 100644 --- a/main.pyw +++ b/main.pyw @@ -1,7 +1,7 @@ import pyglet from source.gui.scene.abc import Scene -from source.gui.widget import Text, FPSDisplay +from source.gui.widget import Text, FPSDisplay, Button from source.gui.window import Window # Test Scene @@ -11,12 +11,47 @@ class TestScene(Scene): def __init__(self, window: "Window"): super().__init__(window) + # loading resource + + normal_texture = pyglet.image.load("./assets/image/button/test_button_normal.png") + hover_texture = pyglet.image.load("./assets/image/button/test_button_hover.png") + click_texture = pyglet.image.load("./assets/image/button/test_button_clicking.png") + + button_atlas = pyglet.image.atlas.TextureAtlas() + normal_region = button_atlas.add(normal_texture) + hover_region = button_atlas.add(hover_texture) + click_region = button_atlas.add(click_texture) + + self.background_batch = pyglet.graphics.Batch() + self.label_batch = pyglet.graphics.Batch() + + # the widgets + self.add_widget(FPSDisplay) - label = self.add_widget(Text, text="Hello World !", x=0.5, y=0.5, width=0.5, height=0.5, anchor_x="center", anchor_y="center") + label = self.add_widget( + Button, + + x=0.5, y=0.5, width=0.5, height=0.5, + + texture_normal=normal_region, + texture_hover=hover_region, + texture_click=click_region, + + label_text="Hello World !", + + background_batch=self.background_batch, + label_batch=self.label_batch, + ) label.on_pressed = lambda button, modifiers: print("pressed", label, button, modifiers) label.on_release = lambda button, modifiers: print("release", label, button, modifiers) + def on_draw(self): + self.background_batch.draw() + self.label_batch.draw() + + + # Create a new window window = Window(resizable=True, vsync=False) diff --git a/source/gui/sprite/Sprite.py b/source/gui/sprite/Sprite.py new file mode 100644 index 0000000..78d1abf --- /dev/null +++ b/source/gui/sprite/Sprite.py @@ -0,0 +1,24 @@ +import pyglet + + +class Sprite(pyglet.sprite.Sprite): + """ + Same as the pyglet sprite, but allow to set a width and height easier + """ + + def __init__(self, width: int = None, height: int = None, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._orig_width: int = self.width + self._orig_height: int = self.height + + if width is not None: self.width = width + if height is not None: self.height = height + + @pyglet.sprite.Sprite.width.setter + def width(self, width: int): + self.scale_x = width / self._orig_width + + @pyglet.sprite.Sprite.height.setter + def height(self, height: int): + self.scale_y = height / self._orig_height diff --git a/source/gui/sprite/__init__.py b/source/gui/sprite/__init__.py new file mode 100644 index 0000000..d2ab302 --- /dev/null +++ b/source/gui/sprite/__init__.py @@ -0,0 +1 @@ +from .Sprite import Sprite diff --git a/source/gui/widget/Button.py b/source/gui/widget/Button.py new file mode 100644 index 0000000..d6f19a2 --- /dev/null +++ b/source/gui/widget/Button.py @@ -0,0 +1,102 @@ +from typing import TYPE_CHECKING, Optional + +import pyglet + +from source.gui.sprite import Sprite +from source.gui.widget.abc import BoxWidget +from source.type import Percentage +from source.utils import dict_prefix + +if TYPE_CHECKING: + from source.gui.scene.abc import Scene + + +class Button(BoxWidget): + """ + A button widget with a background texture that change depending on if it is clicked or hovered, and a label. + You can pass parameter to the background and label by adding "background_" and "label_" before the parameter. + """ + + def __init__(self, scene: "Scene", + + texture_normal: pyglet.image.AbstractImage, + + x: Percentage = 0, + y: Percentage = 0, + width: Percentage = None, + height: Percentage = None, + + texture_hover: pyglet.image.AbstractImage = None, + texture_click: pyglet.image.AbstractImage = None, + + **kwargs): + super().__init__(scene, x, y, width, height) + + self._texture_normal: pyglet.image.AbstractImage = texture_normal + self._texture_hover: Optional[pyglet.image.AbstractImage] = texture_hover + self._texture_click: Optional[pyglet.image.AbstractImage] = texture_click + + self.background = Sprite( + img=self._texture_normal, + x=self.x, y=self.y, width=self.width, height=self.height, + **dict_prefix("background_", kwargs) + ) + + self.label = pyglet.text.Label( + x=self.x, y=self.y, width=self.width, height=self.height, + **dict_prefix("label_", kwargs) + ) + + # background + + @property + def background_texture(self) -> pyglet.image.AbstractImage: + """ + Return the correct texture for the background. + The clicking texture per default, if hover the hovered texture (if it exists) + and if click the clicking texture (if it exists) + :return: the corresponding texture + """ + + return ( + self._texture_click if self.clicking and self._texture_click is not None else + self._texture_hover if self.hovering and self._texture_hover is not None else + self._texture_normal + ) + + def _refresh_background(self) -> None: + self.background.image = self.background_texture + + @BoxWidget.hovering.setter + def hovering(self, hovering: bool): + # when the hover state is changed, update the background + BoxWidget.hovering.fset(self, hovering) + self._refresh_background() + + @BoxWidget.clicking.setter + def clicking(self, clicking: bool): + # when the clicking state is changed, update the background + BoxWidget.clicking.fset(self, clicking) + self._refresh_background() + + # event + + def on_resize(self, width: int, height: int): + self.background.x = self.x + self.background.y = self.y + self.background.width = self.width + self.background.height = self.height + + self.label.x = self.x + self.label.y = self.y + self.label.width = self.width + self.label.height = self.height + + def draw(self): + """ + The draw function. Can be called to draw the widget, but can be ignored to draw it with batchs. + """ + + self.background.draw() + self.label.draw() + diff --git a/source/gui/widget/FPSDisplay.py b/source/gui/widget/FPSDisplay.py index dd0ea51..89b58ad 100644 --- a/source/gui/widget/FPSDisplay.py +++ b/source/gui/widget/FPSDisplay.py @@ -18,5 +18,9 @@ class FPSDisplay(Widget): self.fps_display = pyglet.window.FPSDisplay(scene.window) - def on_draw(self): + def draw(self): + """ + The draw function. Can be called to draw the widget, but can be ignored to draw it with batchs. + """ + self.fps_display.draw() diff --git a/source/gui/widget/Text.py b/source/gui/widget/Text.py index efde644..999f1e4 100644 --- a/source/gui/widget/Text.py +++ b/source/gui/widget/Text.py @@ -34,5 +34,9 @@ class Text(BoxWidget): self.label.width = self.width self.label.height = self.height - def on_draw(self): + def draw(self): + """ + The draw function. Can be called to draw the widget, but can be ignored to draw it with batchs. + """ + self.label.draw() diff --git a/source/gui/widget/__init__.py b/source/gui/widget/__init__.py index 197d78b..4b0ba0b 100644 --- a/source/gui/widget/__init__.py +++ b/source/gui/widget/__init__.py @@ -1,2 +1,3 @@ from .Text import Text from .FPSDisplay import FPSDisplay +from .Button import Button diff --git a/source/gui/widget/abc/BoxWidget.py b/source/gui/widget/abc/BoxWidget.py index 106b66e..2e442f9 100644 --- a/source/gui/widget/abc/BoxWidget.py +++ b/source/gui/widget/abc/BoxWidget.py @@ -30,6 +30,8 @@ class BoxWidget(Widget, ABC): self._hovering = False # is the button currently hovered ? self._clicking = False # is the button currently clicked ? + # property + @property def x(self) -> int: return self.scene.window.width * self._p_x @@ -74,6 +76,24 @@ class BoxWidget(Widget, ABC): def bbox(self) -> tuple[int, int, int, int]: return self.x, self.y, self.x + self.width, self.y + self.height + # property that can be used to add event when these value are modified in some specific widget. + + @property + def hovering(self): + return self._hovering + + @hovering.setter + def hovering(self, hovering: bool): + self._hovering = hovering + + @property + def clicking(self): + return self._clicking + + @clicking.setter + def clicking(self, clicking: bool): + self._clicking = clicking + # event def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): # NOQA @@ -86,11 +106,11 @@ class BoxWidget(Widget, ABC): :dy: the difference of the y mouse axis """ - old_hovering = self._hovering - self._hovering = in_bbox((x, y), self.bbox) + old_hovering = self.hovering + self.hovering = in_bbox((x, y), self.bbox) - if old_hovering != self._hovering: # if the hover changed - if self._hovering: self.on_hover_enter() # call the hover enter event + if old_hovering != self.hovering: # if the hover changed + if self.hovering: self.on_hover_enter() # call the hover enter event else: self.on_hover_leave() # call the hover leave event def on_hover_enter(self): @@ -107,13 +127,13 @@ class BoxWidget(Widget, ABC): # if this button was the one hovered when the click was pressed if not in_bbox((x, y), self.bbox): return - self._clicking = True + self.clicking = True self.on_press(button, modifiers) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): old_click: bool = self._clicking - self._clicking = False + self.clicking = False if not in_bbox((x, y), self.bbox): return diff --git a/source/utils/__init__.py b/source/utils/__init__.py index 3c0dad2..df2ba2b 100644 --- a/source/utils/__init__.py +++ b/source/utils/__init__.py @@ -1,2 +1,3 @@ from .copy_array_offset import copy_array_offset from .in_bbox import in_bbox +from .dict import dict_filter, dict_prefix diff --git a/source/utils/dict.py b/source/utils/dict.py new file mode 100644 index 0000000..2d58294 --- /dev/null +++ b/source/utils/dict.py @@ -0,0 +1,33 @@ +from typing import Callable, Any + + +def dict_filter(filter_func: Callable[[Any, Any], bool], dictionary: dict) -> dict: + """ + Filter a dict object with the filter function given. + :filter_func: the function to filter with + :dictionary: the dictionary to filter + :return: the filtered dictionary + """ + + return { + k: v for k, v in filter( + lambda d: filter_func(d[0], d[1]), + dictionary.items() + ) + } + + +def dict_prefix(prefix: str, dictionary: dict) -> dict: + """ + Take only the keys that start with the prefix, and remove this prefix from the keys. + :prefix: the prefix to use + :dictionary: the dictionary to filter + :return: the dictionary with the prefix + """ + + return { + k.removeprefix(prefix): v for k, v in dict_filter( + lambda k, v: k.startswith(prefix), + dictionary + ).items() + }