added a Widget system with a Button widget example

This commit is contained in:
Faraphel 2023-01-10 00:09:25 +01:00
parent b902d83d90
commit eff0218c8f
30 changed files with 409 additions and 89 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

View file

@ -1,22 +0,0 @@
from typing import Optional
import pyglet
from gui.scene import Scene
from gui.window import Window
class FPSCounterScene(Scene):
"""
This scene represent a simple FPS Counter.
"""
def __init__(self):
self.fps_display: Optional[pyglet.window.FPSDisplay] = None
def on_window_added(self, window: Window):
# the fps display need to be defined here because it is the moment where the window is first accessible
self.fps_display = pyglet.window.FPSDisplay(window=window)
def on_draw(self, window: Window) -> None:
self.fps_display.draw()

View file

@ -1,43 +0,0 @@
import pyglet.event
from gui.window import Window
class Scene(pyglet.event.EventDispatcher):
"""
This class represent a scene that can be applied to a pyglet window.
The scene can represent anything like the main menu, the game, the
options' menu, the multiplayer menu, ...
"""
def on_window_added(self, window: Window): pass # when the Scene is added to a window
def on_window_removed(self, window: Window): pass # when the Scene is removed from a window
def on_draw(self, window: Window): pass
def on_resize(self, window: Window, width: int, height: int): pass
def on_hide(self, window: Window): pass
def on_show(self, window: Window): pass
def on_close(self, window: Window): pass
def on_expose(self, window: Window): pass
def on_activate(self, window: Window): pass
def on_deactivate(self, window: Window): pass
def on_text(self, window: Window, char: str): pass
def on_move(self, window: Window, x: int, y: int): pass
def on_context_lost(self, window: Window): pass
def on_context_state_lost(self, window: Window): pass
def on_key_press(self, window: Window, symbol: int, modifiers: int): pass
def on_key_release(self, window: Window, symbol: int, modifiers: int): pass
def on_key_held(self, window: Window, dt: float, symbol: int, modifiers: int): pass
def on_mouse_enter(self, window: Window, x: int, y: int): pass
def on_mouse_leave(self, window: Window, x: int, y: int): pass
def on_text_motion(self, window: Window, motion: int): pass
def on_text_motion_select(self, window: Window, motion: int): pass
def on_mouse_motion(self, window: Window, x: int, y: int, dx: int, dy: int): pass
def on_mouse_press(self, window: Window, x: int, y: int, button: int, modifiers: int): pass
def on_mouse_release(self, window: Window, x: int, y: int, button: int, modifiers: int): pass
def on_mouse_drag(self, window: Window, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int): pass
def on_mouse_scroll(self, window: Window, x: int, y: int, scroll_x: float, scroll_y: float): pass

View file

@ -1,12 +1,29 @@
import pyglet
from gui.window import Window
from gui.scene import HelloWorldScene, FPSCounterScene
from source.gui.widget.Button import Button
from source.gui.window import Window
from source.gui.scene import HelloWorldScene, FPSCounterScene
# Créer une fenêtre
window = Window(resizable=True, visible=False)
window.add_scene(HelloWorldScene(), FPSCounterScene())
button_normal_image = pyglet.image.load("./assets/test_button_normal.png")
button_hover_image = pyglet.image.load("./assets/test_button_hover.png")
hello_world_scene = HelloWorldScene()
button = Button(
50, 50, 300, 100,
text="HELLO",
on_release=lambda *a, **b: print(a, b),
normal_image=button_normal_image,
hover_image=button_hover_image,
)
hello_world_scene.add_widget(button)
fps_counter_scene = FPSCounterScene()
window.add_scene(hello_world_scene, fps_counter_scene)
# Lance la fenêtre
window.set_visible(True)

View file

@ -1,2 +0,0 @@
from .Boat import Boat
from .Board import Board

View file

@ -1,8 +1,8 @@
import numpy as np
from source import Boat
from source.enums import Orientation, BombState
from source.error import InvalidBoatPosition, PositionAlreadyShot
from source.core import Boat
from source.core.enums import Orientation, BombState
from source.core.error import InvalidBoatPosition, PositionAlreadyShot
from source.utils import copy_array_offset

View file

@ -1,6 +1,6 @@
import numpy as np
from source.enums import Orientation
from source.core.enums import Orientation
class Boat:

0
source/gui/__init__.py Normal file
View file

View file

@ -0,0 +1,30 @@
from typing import Optional, TYPE_CHECKING
import pyglet
from source.gui.scene.base import Scene
if TYPE_CHECKING:
from source.gui.window import Window
class FPSCounterScene(Scene):
"""
This scene represent a simple FPS Counter.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fps_display: Optional[pyglet.window.FPSDisplay] = None
def on_window_added(self, window: "Window"):
super().on_window_added(window)
# the fps display need to be defined here because it is the moment where the window is first accessible
self.fps_display = pyglet.window.FPSDisplay(window=window)
def on_draw(self, window: "Window") -> None:
super().on_draw(window)
self.fps_display.draw()

View file

@ -2,8 +2,12 @@ from datetime import datetime, timedelta
import pyglet
from gui.scene import Scene
from gui.window import Window
from source.gui.scene.base import Scene
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from source.gui.window import Window
class HelloWorldScene(Scene):
@ -14,7 +18,9 @@ class HelloWorldScene(Scene):
The text is centered on the screen.
"""
def __init__(self):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label = pyglet.text.Label(
"Hello World !",
anchor_x="center",
@ -24,17 +30,25 @@ class HelloWorldScene(Scene):
# remember the cooldown for the backspace button
self._hold_backspace_last_call: datetime = datetime.now()
def on_draw(self, window: Window) -> None:
def on_draw(self, window: "Window") -> None:
super().on_draw(window)
self.label.draw()
def on_resize(self, window: Window, width: int, height: int) -> None:
def on_resize(self, window: "Window", width: int, height: int) -> None:
super().on_resize(window, width, height)
self.label.x = width // 2
self.label.y = height // 2
def on_text(self, window: Window, char: str):
def on_text(self, window: "Window", char: str):
super().on_text(window, char)
self.label.text += char
def on_key_held(self, window: Window, dt: float, symbol: int, modifiers: int):
def on_key_held(self, window: "Window", dt: float, symbol: int, modifiers: int):
super().on_key_held(window, dt, symbol, modifiers)
if symbol == pyglet.window.key.BACKSPACE:
# add a cooldown of 0.1 second on the backspace key

View file

@ -0,0 +1,18 @@
from source.gui.scene.base import Scene
from source.gui.widget.Button import Button
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from source.gui.window import Window
class MainMenuScene(Scene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_widget(Button())
def on_draw(self, window: "Window"):
super().on_draw(window)

View file

@ -1,3 +1,2 @@
from .Scene import Scene
from .HelloWorldScene import HelloWorldScene
from .FPSCounterScene import FPSCounterScene

View file

@ -0,0 +1,116 @@
from pyglet.event import EventDispatcher
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from source.gui.widget.base import Widget
from source.gui.window import Window
class Scene(EventDispatcher):
"""
This class represent a scene that can be applied to a pyglet window.
The scene can represent anything like the main menu, the game, the
options' menu, the multiplayer menu, ...
"""
def __init__(self, widgets: list["Widget"] = None):
self._widgets: list["Widget"] = []
if widgets is not None: self.add_widget(*widgets)
def add_widget(self, *widgets: "Widget", priority: int = 0) -> None:
for widget in widgets:
self._widgets.insert(priority, widget)
widget.on_scene_added(self)
def remove_widget(self, *widgets: "Widget") -> None:
for widget in widgets:
widget.on_scene_removed(self)
self._widgets.remove(widget)
def clear_widget(self) -> None:
for widget in self._widgets: widget.on_scene_removed(self)
self._widgets.clear()
# scene event
def on_window_added(self, window: "Window"): # when the Scene is added to a window
for widget in self._widgets: widget.on_window_added(window, self)
def on_window_removed(self, window: "Window"): # when the Scene is removed from a window
for widget in self._widgets: widget.on_window_removed(window, self)
# window event
def on_draw(self, window: "Window"):
for widget in self._widgets: widget.on_draw(window, self)
def on_resize(self, window: "Window", width: int, height: int):
for widget in self._widgets: widget.on_resize(window, self, width, height)
def on_hide(self, window: "Window"):
for widget in self._widgets: widget.on_hide(window, self)
def on_show(self, window: "Window"):
for widget in self._widgets: widget.on_show(window, self)
def on_close(self, window: "Window"):
for widget in self._widgets: widget.on_close(window, self)
def on_expose(self, window: "Window"):
for widget in self._widgets: widget.on_expose(window, self)
def on_activate(self, window: "Window"):
for widget in self._widgets: widget.on_activate(window, self)
def on_deactivate(self, window: "Window"):
for widget in self._widgets: widget.on_deactivate(window, self)
def on_text(self, window: "Window", char: str):
for widget in self._widgets: widget.on_text(window, self, char)
def on_move(self, window: "Window", x: int, y: int):
for widget in self._widgets: widget.on_move(window, self, x, y)
def on_context_lost(self, window: "Window"):
for widget in self._widgets: widget.on_context_lost(window, self)
def on_context_state_lost(self, window: "Window"):
for widget in self._widgets: widget.on_context_state_lost(window, self)
def on_key_press(self, window: "Window", symbol: int, modifiers: int):
for widget in self._widgets: widget.on_key_press(window, self, symbol, modifiers)
def on_key_release(self, window: "Window", symbol: int, modifiers: int):
for widget in self._widgets: widget.on_key_release(window, self, symbol, modifiers)
def on_key_held(self, window: "Window", dt: float, symbol: int, modifiers: int):
for widget in self._widgets: widget.on_key_held(window, self, dt, symbol, modifiers)
def on_mouse_enter(self, window: "Window", x: int, y: int):
for widget in self._widgets: widget.on_mouse_enter(window, self, x, y)
def on_mouse_leave(self, window: "Window", x: int, y: int):
for widget in self._widgets: widget.on_mouse_leave(window, self, x, y)
def on_text_motion(self, window: "Window", motion: int):
for widget in self._widgets: widget.on_text_motion(window, self, motion)
def on_text_motion_select(self, window: "Window", motion: int):
for widget in self._widgets: widget.on_text_motion_select(window, self, motion)
def on_mouse_motion(self, window: "Window", x: int, y: int, dx: int, dy: int):
for widget in self._widgets: widget.on_mouse_motion(window, self, x, y, dx, dy)
def on_mouse_press(self, window: "Window", x: int, y: int, button: int, modifiers: int):
for widget in self._widgets: widget.on_mouse_press(window, self, x, y, button, modifiers)
def on_mouse_release(self, window: "Window", x: int, y: int, button: int, modifiers: int):
for widget in self._widgets: widget.on_mouse_release(window, self, x, y, button, modifiers)
def on_mouse_drag(self, window: "Window", x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int):
for widget in self._widgets: widget.on_mouse_drag(window, self, x, y, dx, dy, buttons, modifiers)
def on_mouse_scroll(self, window: "Window", x: int, y: int, scroll_x: float, scroll_y: float):
for widget in self._widgets: widget.on_mouse_scroll(window, self, x, y, scroll_x, scroll_y)

View file

@ -0,0 +1 @@
from .Scene import Scene

123
source/gui/widget/Button.py Normal file
View file

@ -0,0 +1,123 @@
from typing import Callable, Optional, TYPE_CHECKING
import pyglet
from source.gui.widget.base import Widget
from source.utils import in_bbox
if TYPE_CHECKING:
from source.gui.scene.base import Scene
from source.gui.window import Window
class Button(Widget):
__slots__ = ("_x", "_y", "_width", "_height", "_text", "on_press", "on_release", "_normal_image", "_hover_image",
"_hovering")
def __init__(self,
x: int,
y: int,
width: int,
height: int,
on_press: Optional[Callable] = None,
on_release: Optional[Callable] = None,
normal_image: pyglet.image.AbstractImage = None,
hover_image: pyglet.image.AbstractImage = None,
*args, **kwargs
):
# TODO: use batch
# TODO: make the label centered in the button
# TODO: use texture bin and animation to simplify the image handling ?
self.label = pyglet.text.Label(*args, **kwargs)
self._hovering = False
self._normal_sprite = pyglet.sprite.Sprite(normal_image)
self._hover_sprite = pyglet.sprite.Sprite(hover_image)
self.on_press: Optional[Callable[["Window", "Scene", int, int, int, int], None]] = on_press
self.on_release: Optional[Callable[["Window", "Scene", int, int, int, int], None]] = on_release
self.x: int = x
self.y: int = y
self.width: int = width
self.height: int = height
# function
def _update_sprite_size(self, x: int = None, y: int = None, width: int = None, height: int = None):
for sprite in self._normal_sprite, self._hover_sprite:
sprite.update(
x=x,
y=y,
scale_x=None if width is None else width / sprite.width,
scale_y=None if height is None else height / sprite.height,
)
# button getter and setter
@property
def background_sprite(self) -> Optional[pyglet.sprite.Sprite]:
return self._hover_sprite if self._hovering else self._normal_sprite
@property
def bbox(self) -> tuple[int, int, int, int]:
return self.x, self.y, self.x + self.width, self.y + self.height
# label getter and setter
@property
def x(self) -> int: return self._x
@x.setter
def x(self, value: int):
self._x = value
self.label.x = value
self._update_sprite_size(x=value)
@property
def y(self) -> int: return self._y
@y.setter
def y(self, value: int):
self._y = value
self.label.y = value
self._update_sprite_size(y=value)
@property
def width(self) -> int: return self._width
@width.setter
def width(self, value: int):
self._width = value
self.label.width = value
self._update_sprite_size(width=value)
@property
def height(self) -> int: return self._height
@height.setter
def height(self, value: int):
self._height = value
self.label.height = value
self._update_sprite_size(height=value)
# event
def on_mouse_press(self, window: "Window", scene: "Scene", x: int, y: int, button: int, modifiers: int):
if not in_bbox((x, y), self.bbox): return
if self.on_press is not None: self.on_press(window, scene, x, y, button, modifiers)
def on_mouse_release(self, window: "Window", scene: "Scene", x: int, y: int, button: int, modifiers: int):
if not in_bbox((x, y), self.bbox): return
if self.on_release is not None: self.on_release(window, scene, x, y, button, modifiers)
def on_mouse_motion(self, window: "Window", scene: "Scene", x: int, y: int, dx: int, dy: int):
self._hovering = in_bbox((x, y), self.bbox)
def on_draw(self, window: "Window", scene: "Scene"):
if (bg := self.background_sprite) is not None:
bg.draw()
self.label.draw()

View file

View file

@ -0,0 +1,50 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from source.gui.scene.base import Scene
from source.gui.window import Window
class Widget:
"""
This class represent a widget that can be attached to a Scene.
It can be used to create a button, a label, ...
"""
# widget event
def on_scene_added(self, scene: "Scene"): pass
def on_scene_removed(self, scene: "Scene"): pass
# scene event
def on_window_added(self, window: "Window", scene: "Scene"): pass
def on_window_removed(self, window: "Window", scene: "Scene"): pass
# global event
def on_draw(self, window: "Window", scene: "Scene"): pass
def on_resize(self, window: "Window", scene: "Scene", width: int, height: int): pass
def on_hide(self, window: "Window", scene: "Scene"): pass
def on_show(self, window: "Window", scene: "Scene"): pass
def on_close(self, window: "Window", scene: "Scene"): pass
def on_expose(self, window: "Window", scene: "Scene"): pass
def on_activate(self, window: "Window", scene: "Scene"): pass
def on_deactivate(self, window: "Window", scene: "Scene"): pass
def on_text(self, window: "Window", scene: "Scene", char: str): pass
def on_move(self, window: "Window", scene: "Scene", x: int, y: int): pass
def on_context_lost(self, window: "Window", scene: "Scene"): pass
def on_context_state_lost(self, window: "Window", scene: "Scene"): pass
def on_key_press(self, window: "Window", scene: "Scene", symbol: int, modifiers: int): pass
def on_key_release(self, window: "Window", scene: "Scene", symbol: int, modifiers: int): pass
def on_key_held(self, window: "Window", scene: "Scene", dt: float, symbol: int, modifiers: int): pass
def on_mouse_enter(self, window: "Window", scene: "Scene", x: int, y: int): pass
def on_mouse_leave(self, window: "Window", scene: "Scene", x: int, y: int): pass
def on_text_motion(self, window: "Window", scene: "Scene", motion: int): pass
def on_text_motion_select(self, window: "Window", scene: "Scene", motion: int): pass
def on_mouse_motion(self, window: "Window", scene: "Scene", x: int, y: int, dx: int, dy: int): pass
def on_mouse_press(self, window: "Window", scene: "Scene", x: int, y: int, button: int, modifiers: int): pass
def on_mouse_release(self, window: "Window", scene: "Scene", x: int, y: int, button: int, modifiers: int): pass
def on_mouse_drag(self, window: "Window", scene: "Scene", x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int): pass
def on_mouse_scroll(self, window: "Window", scene: "Scene", x: int, y: int, scroll_x: float, scroll_y: float): pass

View file

@ -0,0 +1 @@
from .Widget import Widget

View file

@ -1,8 +1,9 @@
from typing import Optional, Callable
from typing import Optional, Callable, TYPE_CHECKING
import pyglet.window
from gui.scene import Scene
if TYPE_CHECKING:
from source.gui.scene import Scene
class Window(pyglet.window.Window): # NOQA - pycharm think pyglet window is abstract
@ -14,9 +15,10 @@ class Window(pyglet.window.Window): # NOQA - pycharm think pyglet window is abs
putting everything in the window code.
"""
def __init__(self, *args, scenes: Optional[Scene] = None, **kwargs):
def __init__(self, scenes: Optional["Scene"] = None, *args, **kwargs):
super().__init__(*args, **kwargs)
self._scenes: list[Scene] = [] if scenes is None else scenes
self._scenes: list["Scene"] = []
if scenes is not None: self.add_scene(*scenes)
# add a keys handler to the window
self.keys = pyglet.window.key.KeyStateHandler()
@ -27,7 +29,7 @@ class Window(pyglet.window.Window): # NOQA - pycharm think pyglet window is abs
# scene methods
def set_scene(self, *scenes: Scene) -> None:
def set_scene(self, *scenes: "Scene") -> None:
"""
Set the scene(s) of the window
:param scenes: the scene(s) to set
@ -42,7 +44,7 @@ class Window(pyglet.window.Window): # NOQA - pycharm think pyglet window is abs
for scene in self._scenes: scene.on_window_removed(self)
self._scenes.clear()
def add_scene(self, *scenes: Scene, priority: int = 0) -> None:
def add_scene(self, *scenes: "Scene", priority: int = 0) -> None:
"""
Add a scene to the window
:param scenes: the scene to add
@ -52,7 +54,7 @@ class Window(pyglet.window.Window): # NOQA - pycharm think pyglet window is abs
self._scenes.insert(priority, scene)
scene.on_window_added(self)
def remove_scene(self, *scenes: Scene) -> None:
def remove_scene(self, *scenes: "Scene") -> None:
"""
Remove a scene from the window
:param scenes: the scene to remove

View file

@ -1 +1,2 @@
from .Window import Window

View file

@ -1 +1,2 @@
from .copy_array_offset import copy_array_offset
from .in_bbox import in_bbox

14
source/utils/in_bbox.py Normal file
View file

@ -0,0 +1,14 @@
def in_bbox(point: tuple[int, int], bbox: tuple[int, int, int, int]) -> bool:
"""
Return true if a point is inside a bounding box
:param point: the point to check
:param bbox: the bbox where to check the point
:return: True if the point is inside the bbox, False otherwise
"""
point_x, point_y = point
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = bbox
if not bbox_x1 <= point_x <= bbox_x2: return False
if not bbox_y1 <= point_y <= bbox_y2: return False
return True