diff --git a/source/event/Listener.py b/source/event/Listener.py new file mode 100644 index 0000000..3b90e52 --- /dev/null +++ b/source/event/Listener.py @@ -0,0 +1,17 @@ +from typing import Callable + + +class Listener: + def __init__(self): + self._events_listener: dict[str, set[Callable]] = {} + + def add_listener(self, name: str, callback: Callable): + if name not in self._events_listener: self._events_listener[name] = set() + self._events_listener[name].add(callback) + + def remove_listener(self, name: str, callback: Callable): + self._events_listener[name].remove(callback) + + def trigger_event(self, name: str, *args, **kwargs): + for listener in self._events_listener.get(name, set()): + listener(*args, **kwargs) diff --git a/source/event/__init__.py b/source/event/__init__.py new file mode 100644 index 0000000..b05b661 --- /dev/null +++ b/source/event/__init__.py @@ -0,0 +1 @@ +from .Listener import Listener diff --git a/source/gui/scene/MainMenu.py b/source/gui/scene/MainMenu.py index adbe64a..a88f986 100644 --- a/source/gui/scene/MainMenu.py +++ b/source/gui/scene/MainMenu.py @@ -45,7 +45,7 @@ class MainMenu(Scene): texture_click=texture_button_click ) - self.game_create.on_release = lambda *_: self.window.set_scene(scene.RoomCreate) + self.game_create.add_listener("on_click_release", lambda *_: self.window.set_scene(scene.RoomCreate)) self.game_join = self.add_widget( widget.Button, @@ -60,7 +60,7 @@ class MainMenu(Scene): texture_click=texture_button_click ) - self.game_join.on_release = lambda *_: self.window.set_scene(scene.RoomJoin) + self.game_join.add_listener("on_click_release", lambda *_: self.window.set_scene(scene.RoomJoin)) self.settings = self.add_widget( widget.Button, @@ -75,7 +75,7 @@ class MainMenu(Scene): texture_click=texture_button_click ) - self.settings.on_release = lambda *_: self.window.set_scene(scene.Settings) + self.settings.add_listener("on_click_release", lambda *_: self.window.set_scene(scene.Settings)) self.fps_display = self.add_widget(FPSDisplay, color=(255, 255, 255, 180)) diff --git a/source/gui/scene/RoomCreate.py b/source/gui/scene/RoomCreate.py index 35f2eeb..0e43c1d 100644 --- a/source/gui/scene/RoomCreate.py +++ b/source/gui/scene/RoomCreate.py @@ -36,7 +36,7 @@ class RoomCreate(Scene): ) from source.gui.scene import MainMenu - self.back.on_release = lambda *_: self.window.set_scene(MainMenu) + self.back.add_listener("on_click_release", lambda *_: self.window.set_scene(MainMenu)) self.label_ip = self.add_widget( widget.Text, diff --git a/source/gui/scene/RoomJoin.py b/source/gui/scene/RoomJoin.py index 78e94f7..a27915e 100644 --- a/source/gui/scene/RoomJoin.py +++ b/source/gui/scene/RoomJoin.py @@ -34,7 +34,7 @@ class RoomJoin(Scene): ) from source.gui.scene import MainMenu - self.back.on_release = lambda *_: self.window.set_scene(MainMenu) + self.back.add_listener("on_click_release", lambda *_: self.window.set_scene(MainMenu)) self.entry_ip = self.add_widget( widget.Input, @@ -69,12 +69,12 @@ class RoomJoin(Scene): texture_click=texture_button_click ) - self.connect.on_release = lambda *_: network.Client( + self.connect.add_listener("on_click_release", lambda *_: network.Client( window=self.window, ip_address=self.entry_ip.text, daemon=True, username="Client" - ).start() + ).start()) def on_draw(self): self.back.draw() diff --git a/source/gui/scene/Settings.py b/source/gui/scene/Settings.py index 939adb6..1bc2f9c 100644 --- a/source/gui/scene/Settings.py +++ b/source/gui/scene/Settings.py @@ -37,7 +37,7 @@ class Settings(Scene): ) from source.gui.scene import MainMenu - self.back.on_release = lambda *_: self.window.set_scene(MainMenu) + self.back.add_listener("on_click_release", lambda *_: self.window.set_scene(MainMenu)) self.checkbox = self.add_widget( widget.Checkbox, diff --git a/source/gui/widget/Button.py b/source/gui/widget/Button.py index 2b786eb..7915560 100644 --- a/source/gui/widget/Button.py +++ b/source/gui/widget/Button.py @@ -47,6 +47,9 @@ class Button(BoxWidget): **dict_prefix("label_", kwargs) ) + self.add_listener("on_hover_change", lambda *_: self._refresh_background()) + self.add_listener("on_click_change", lambda *_: self._refresh_background()) + self._refresh_size() # refresh the size and position for the background and label # background @@ -76,20 +79,7 @@ class Button(BoxWidget): self.background.width, self.background.height = self.size # center the label - self.label.x = self.x + (self.width / 2) - self.label.y = self.y + (self.height / 2) - - @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() + self.label.x, self.label.y = self.center # event diff --git a/source/gui/widget/Checkbox.py b/source/gui/widget/Checkbox.py index 59aaeb8..6dff2ad 100644 --- a/source/gui/widget/Checkbox.py +++ b/source/gui/widget/Checkbox.py @@ -34,6 +34,8 @@ class Checkbox(BoxWidget): self.state = state + self.add_listener("on_click_release", lambda *_: self.swap_state()) + self._refresh_size() # refreshing @@ -60,14 +62,13 @@ class Checkbox(BoxWidget): self._state = state self._refresh_tick() + def swap_state(self): + self.state = not self.state # inverse l'état + # event def on_resize(self, width: int, height: int): self._refresh_size() - def on_release(self, rel_x: int, rel_y: int, button: int, modifiers: int): - # lorsque le bouton est enclenché, inverse son état - self.state = not self.state - def draw(self): self.tick.draw() diff --git a/source/gui/widget/GameGrid.py b/source/gui/widget/GameGrid.py index d21e489..75a920c 100644 --- a/source/gui/widget/GameGrid.py +++ b/source/gui/widget/GameGrid.py @@ -49,6 +49,11 @@ class GameGrid(BoxWidget): **dict_prefix("cursor_", kwargs) ) + self.add_listener("on_hover_leave", lambda *_: self.hide_cursor()) + self.add_listener("on_hover", self._refresh_cursor) + self.add_listener("on_click_release", + lambda rel_x, rel_y, *_: print("click", self.get_cell_from_rel(rel_x, rel_y))) + self._refresh_size() def get_cell_from_rel(self, rel_x: int, rel_y: int) -> tuple[int, int]: @@ -68,14 +73,24 @@ class GameGrid(BoxWidget): line.x = self.x + self.cell_width * column line.x2 = line.x line.y = self.y - line.y2 = self.y + self.height + line.y2 = self.y2 for row, line in enumerate(self.lines[-self._rows+1:], start=1): line.x = self.x - line.x2 = self.x + self.width + line.x2 = self.x2 line.y = self.y + self.cell_height * row line.y2 = line.y + def _refresh_cursor(self, rel_x: int, rel_y: int): + cell_x, cell_y = self.get_cell_from_rel(rel_x, rel_y) + + self.cursor.x = self.x + cell_x * self.width / self._columns + self.cursor.y = self.y + cell_y * self.height / self._rows + self.cursor.width, self.cursor.height = self.cell_size + + def hide_cursor(self): + self.cursor.width, self.cursor.height = 0, 0 + # property @property @@ -92,19 +107,6 @@ class GameGrid(BoxWidget): # event - def on_hover(self, rel_x: int, rel_y: int): - cell_x, cell_y = self.get_cell_from_rel(rel_x, rel_y) - - self.cursor.x = self.x + cell_x * self.width / self._columns - self.cursor.y = self.y + cell_y * self.height / self._rows - self.cursor.width, self.cursor.height = self.cell_size - - def on_hover_leave(self, rel_x: int, rel_y: int): - self.cursor.width, self.cursor.height = 0, 0 - - def on_release(self, rel_x: int, rel_y: int, button: int, modifiers: int): - print("click", (rel_x, rel_y), self.get_cell_from_rel(rel_x, rel_y)) - def on_resize(self, width: int, height: int): self._refresh_size() diff --git a/source/gui/widget/Input.py b/source/gui/widget/Input.py index 7cfac8d..e1fc693 100644 --- a/source/gui/widget/Input.py +++ b/source/gui/widget/Input.py @@ -47,6 +47,8 @@ class Input(BoxWidget): **dict_prefix("label_", kwargs) ) + self.add_listener("on_activate_change", lambda *_: self._refresh_background()) + self._refresh_size() # background @@ -76,13 +78,7 @@ class Input(BoxWidget): self.background.width, self.background.height = self.size # center the label - self.label.x = self.x + (self.width / 2) - self.label.y = self.y + (self.height / 2) - - @BoxWidget.activated.setter - def activated(self, activated: bool) -> None: - BoxWidget.activated.fset(self, activated) - self._refresh_background() + self.label.x, self.label.y = self.center # property diff --git a/source/gui/widget/Scroller.py b/source/gui/widget/Scroller.py index a9c4532..3151c18 100644 --- a/source/gui/widget/Scroller.py +++ b/source/gui/widget/Scroller.py @@ -48,6 +48,8 @@ class Scroller(BoxWidget): **dict_prefix("label_", kwargs) ) + self.add_listener("on_click_release", lambda rel_x, *_: self._refresh_cursor(rel_x)) + self._from = from_ self._to = to self.value = value @@ -73,15 +75,14 @@ class Scroller(BoxWidget): ) # label - self.label.x = self.x + (self.width / 2) - self.label.y = self.y + (self.height / 2) + self.label.x, self.label.y = self.center self.label.text = str(self.text_transform(self.value)) - # property - - def on_pressed(self, rel_x: int, rel_y: int, button: int, modifiers: int): + def _refresh_cursor(self, rel_x: int): self.value = rel_x / self.width + # property + @property def value(self): return self._value diff --git a/source/gui/widget/abc/BoxWidget.py b/source/gui/widget/abc/BoxWidget.py index 1cf53cc..fc614c4 100644 --- a/source/gui/widget/abc/BoxWidget.py +++ b/source/gui/widget/abc/BoxWidget.py @@ -27,9 +27,9 @@ class BoxWidget(Widget, ABC): self.width = width self.height = height - self._hovering = False # is the button currently hovered ? - self._clicking = False # is the button currently clicked ? - self._activated = False # is the button activated ? (the last click was inside this widget) + self.hovering = False # is the button currently hovered ? + self.clicking = False # is the button currently clicked ? + self.activated = False # is the button activated ? (the last click was inside this widget) # property @@ -64,6 +64,22 @@ class BoxWidget(Widget, ABC): def y(self, y: Distance): self._y = y + @property + def xy(self) -> tuple[int, int]: + return self.x, self.y + + @property + def x2(self) -> int: + return self.x + self.width + + @property + def y2(self) -> int: + return self.y + self.height + + @property + def xy2(self) -> tuple[int, int]: + return self.x2, self.y2 + @property def width(self) -> int: return self._getter_distance(self.scene.window.width, self._width) @@ -80,47 +96,29 @@ class BoxWidget(Widget, ABC): def height(self, height: Optional[Distance]): self._height = height - @property - def xy(self) -> tuple[int, int]: - return self.x, self.y - @property def size(self) -> tuple[int, int]: return self.width, self.height @property 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. + return self.x, self.y, self.x2, self.y2 @property - def hovering(self): - return self._hovering - - @hovering.setter - def hovering(self, hovering: bool): - self._hovering = hovering + def center_x(self) -> float: + return self.x + (self.width / 2) @property - def clicking(self): - return self._clicking - - @clicking.setter - def clicking(self, clicking: bool): - self._clicking = clicking + def center_y(self) -> float: + return self.y + (self.height / 2) @property - def activated(self): - return self._activated - - @activated.setter - def activated(self, activated: bool): - self._activated = activated + def center(self) -> tuple[float, float]: + return self.center_x, self.center_y # event - def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): # NOQA + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): """ When the mouse is moved, this event is triggered. Allow the implementation of the on_hover_enter and on_hover_leave events @@ -130,60 +128,44 @@ class BoxWidget(Widget, ABC): :dy: the difference of the y mouse axis """ + rel_x, rel_y = x - self.x, y - self.y + old_hovering = self.hovering self.hovering = in_bbox((x, y), self.bbox) - rel_x, rel_y = x - self.x, y - self.y - if old_hovering != self.hovering: # if the hover changed - if self.hovering: self.on_hover_enter(rel_x, rel_y) # call the hover enter event - else: self.on_hover_leave(rel_x, rel_y) # call the hover leave event + # call the hover changed event + self.trigger_event("on_hover_change", rel_x, rel_y) + # call the hover enter / leave event + self.trigger_event("on_hover_enter" if self.hovering else "on_hover_leave", rel_x, rel_y) if self.hovering: # if the mouse motion is inside the collision - self.on_hover(rel_x, rel_y) # call the hover event - - def on_hover(self, rel_x: int, rel_y: int): - """ - This event is called when the mouse move in the bbox of the widget - """ - - def on_hover_enter(self, rel_x: int, rel_y: int): - """ - This event is called when the mouse enter the bbox of the widget - """ - - def on_hover_leave(self, rel_x: int, rel_y: int): - """ - This event is called when the mouse leave the bbox of the widget - """ + self.trigger_event("on_hover", rel_x, rel_y) # call the hover event def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): - # if this button was the one hovered when the click was pressed + rel_x, rel_y = x - self.x, y - self.y - if not in_bbox((x, y), self.bbox): - self.activated = False # if the click was not in the bbox, disable the activated state - return + self.activated = in_bbox((x, y), self.bbox) + self.trigger_event("on_activate_change", rel_x, rel_y, button, modifiers) - self.activated = True # if the click is inside the bbox, enable the activated state - self.clicking = True # the widget is now clicked + if self.activated: # if the click was inside the widget + self.trigger_event("on_activate_enter", rel_x, rel_y, button, modifiers) - self.on_pressed(x - self.x, y - self.y, button, modifiers) + self.clicking = True # the widget is also now clicked + self.trigger_event("on_click_change", rel_x, rel_y, button, modifiers) + self.trigger_event("on_click_press", rel_x, rel_y, button, modifiers) + + else: + self.trigger_event("on_activate_leave", rel_x, rel_y, button, modifiers) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): - old_click: bool = self._clicking + rel_x, rel_y = x - self.x, y - self.y + + old_click: bool = self.clicking self.clicking = False # the widget is no longer clicked - if not in_bbox((x, y), self.bbox): return + if not in_bbox((x, y), self.bbox): return # if the release was not in the collision, ignore - # if this button was the one hovered when the click was pressed - if old_click: self.on_release(x - self.x, y - self.y, button, modifiers) - - def on_pressed(self, rel_x: int, rel_y: int, button: int, modifiers: int): - """ - This event is called when the bbox is pressed - """ - - def on_release(self, rel_x: int, rel_y: int, button: int, modifiers: int): - """ - This event is called when the bbox is released - """ \ No newline at end of file + if old_click: # if this button was the one hovered when the click was pressed + self.trigger_event("on_click_change", rel_x, rel_y, button, modifiers) + self.trigger_event("on_click_release", rel_x, rel_y, button, modifiers) diff --git a/source/gui/widget/abc/Widget.py b/source/gui/widget/abc/Widget.py index 590272e..a801deb 100644 --- a/source/gui/widget/abc/Widget.py +++ b/source/gui/widget/abc/Widget.py @@ -1,11 +1,13 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING +from source.event import Listener + if TYPE_CHECKING: from source.gui.scene.abc import Scene -class Widget(ABC): +class Widget(Listener, ABC): """ A Widget that can be attached to a scene. @@ -13,6 +15,8 @@ class Widget(ABC): """ def __init__(self, scene: "Scene", **kwargs): + super().__init__() + self.scene = scene @abstractmethod