From 3b34bcac26d83f92a442ce381c3deb1f77a47c37 Mon Sep 17 00:00:00 2001 From: Maic Siemering Date: Fri, 10 May 2024 22:52:30 +0200 Subject: [PATCH] feat(gui): UIInteractive interactions limited to left mouse button --- arcade/gui/widgets/__init__.py | 93 +++++++---------------------- tests/unit/gui/test_interactions.py | 25 +++++++- 2 files changed, 45 insertions(+), 73 deletions(-) diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 43fc5a17d..3e88a0b6c 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -3,24 +3,13 @@ from abc import ABC from math import floor from random import randint -from typing import ( - NamedTuple, - Iterable, - Optional, - Union, - TYPE_CHECKING, - TypeVar, - Tuple, - List, - Dict, - Callable -) +from typing import NamedTuple, Iterable, Optional, Union, TYPE_CHECKING, TypeVar, Tuple, List, Dict, Callable from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED from typing_extensions import Self import arcade -from arcade import Sprite, get_window, Texture +from arcade import Sprite, Texture from arcade.color import TRANSPARENT_BLACK from arcade.gui.events import ( UIEvent, @@ -81,11 +70,7 @@ def collide_with_point(self, x: AsFloat, y: AsFloat) -> bool: left, bottom, width, height = self return left <= x <= left + width and bottom <= y <= bottom + height - def scale( - self, - scale: float, - rounding: Optional[Callable[..., float]] = floor - ) -> "Rect": + def scale(self, scale: float, rounding: Optional[Callable[..., float]] = floor) -> "Rect": """Return a new rect scaled relative to the origin. By default, the new rect's values are rounded down to whole @@ -115,11 +100,7 @@ def scale( height * scale, ) - def resize( - self, - width: float | None = None, - height: float | None = None - ) -> "Rect": + def resize(self, width: float | None = None, height: float | None = None) -> "Rect": """Return a rect with a new width or height but same lower left. Fix x and y coordinate. @@ -213,11 +194,7 @@ def align_center_y(self, value: AsFloat) -> "Rect": diff_y = value - self.center_y return self.move(dy=diff_y) - def min_size( - self, - width: Optional[AsFloat] = None, - height: Optional[AsFloat] = None - ) -> "Rect": + def min_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "Rect": """ Sets the size to at least the given min values. """ @@ -228,11 +205,7 @@ def min_size( max(height or 0.0, self.height), ) - def max_size( - self, - width: Optional[AsFloat] = None, - height: Optional[AsFloat] = None - ) -> "Rect": + def max_size(self, width: Optional[AsFloat] = None, height: Optional[AsFloat] = None) -> "Rect": """ Limits the size to the given max values. """ @@ -333,9 +306,7 @@ def __init__( self.add(child) bind(self, "rect", self.trigger_full_render) - bind( - self, "visible", self.trigger_full_render - ) # TODO maybe trigger_parent_render would be enough + bind(self, "visible", self.trigger_full_render) # TODO maybe trigger_parent_render would be enough bind(self, "_children", self.trigger_render) bind(self, "_border_width", self.trigger_render) bind(self, "_border_color", self.trigger_render) @@ -364,9 +335,7 @@ def add(self, child: W, **kwargs) -> W: else: if not 0 <= index <= len(self.children): raise ValueError("Index must be between 0 and the number of children") - self._children.insert( - index, _ChildEntry(child, kwargs) - ) + self._children.insert(index, _ChildEntry(child, kwargs)) return child @@ -478,9 +447,7 @@ def do_render_base(self, surface: Surface): surface.clear(self._bg_color) # draw background texture if self._bg_tex: - surface.draw_texture( - x=0, y=0, width=self.width, height=self.height, tex=self._bg_tex - ) + surface.draw_texture(x=0, y=0, width=self.width, height=self.height, tex=self._bg_tex) # draw border if self._border_width and self._border_color: @@ -675,21 +642,11 @@ def content_size(self): @property def content_width(self): - return ( - self.rect.width - - 2 * self._border_width - - self._padding_left - - self._padding_right - ) + return self.rect.width - 2 * self._border_width - self._padding_left - self._padding_right @property def content_height(self): - return ( - self.rect.height - - 2 * self._border_width - - self._padding_top - - self._padding_bottom - ) + return self.rect.height - 2 * self._border_width - self._padding_top - self._padding_bottom @property def content_rect(self): @@ -777,19 +734,24 @@ def on_event(self, event: UIEvent) -> Optional[bool]: if isinstance(event, UIMouseMovementEvent): self.hovered = self.rect.collide_with_point(event.x, event.y) - if isinstance(event, UIMousePressEvent) and self.rect.collide_with_point( - event.x, event.y + if ( + isinstance(event, UIMousePressEvent) + and self.rect.collide_with_point(event.x, event.y) + and event.button == arcade.MOUSE_BUTTON_LEFT ): self.pressed = True return EVENT_HANDLED - if self.pressed and isinstance(event, UIMouseReleaseEvent): + if self.pressed and isinstance(event, UIMouseReleaseEvent) and event.button == arcade.MOUSE_BUTTON_LEFT: self.pressed = False if self.rect.collide_with_point(event.x, event.y): if not self.disabled: # Dispatch new on_click event, source is this widget itself self.dispatch_event( - "on_click", UIOnClickEvent(self, event.x, event.y) + "on_click", + UIOnClickEvent( + source=self, x=event.x, y=event.y, button=event.button, modifiers=event.modifiers + ), ) # TODO unsure if it makes more sense to mark the event handled if the click event is handled. return EVENT_HANDLED @@ -845,6 +807,7 @@ def __init__( ) self.color: RGBA255 = (randint(0, 255), randint(0, 255), randint(0, 255), 255) self.border_color = arcade.color.BATTLESHIP_GREY + self.border_width = 0 def on_click(self, event: UIOnClickEvent): print("UIDummy.rect:", self.rect) @@ -852,24 +815,12 @@ def on_click(self, event: UIOnClickEvent): def on_update(self, dt): self.border_width = 2 if self.hovered else 0 - self.border_color = ( - arcade.color.WHITE if self.pressed else arcade.color.BATTLESHIP_GREY - ) + self.border_color = arcade.color.WHITE if self.pressed else arcade.color.BATTLESHIP_GREY def do_render(self, surface: Surface): self.prepare_render(surface) surface.clear(self.color) - # if self.hovered: - # arcade.draw_xywh_rectangle_outline( - # 0, - # 0, - # self.width, - # self.height, - # color=arcade.color.BATTLESHIP_GREY, - # border_width=3, - # ) - class UISpriteWidget(UIWidget): """Create a UI element with a sprite that controls what is displayed. diff --git a/tests/unit/gui/test_interactions.py b/tests/unit/gui/test_interactions.py index e1eb890c4..96cd0570c 100644 --- a/tests/unit/gui/test_interactions.py +++ b/tests/unit/gui/test_interactions.py @@ -1,6 +1,7 @@ from typing import List from unittest.mock import Mock +import arcade from arcade.gui.events import UIEvent, UIOnClickEvent, UIMousePressEvent, UIMouseReleaseEvent from arcade.gui.widgets import UIDummy from . import record_ui_events @@ -33,7 +34,7 @@ def test_overlapping_hover_on_widget(uimanager): assert widget2.hovered is True -def test_click_on_widget(uimanager): +def test_left_click_on_widget(uimanager): # GIVEN widget1 = UIDummy() widget1.on_click = Mock() @@ -41,7 +42,7 @@ def test_click_on_widget(uimanager): # WHEN with record_ui_events(widget1, "on_event", "on_click") as records: - uimanager.click(widget1.center_x, widget1.center_y) + uimanager.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_LEFT) # THEN records: List[UIEvent] @@ -54,10 +55,30 @@ def test_click_on_widget(uimanager): assert click_event.source == widget1 assert click_event.x == widget1.center_x assert click_event.y == widget1.center_y + assert click_event.button == arcade.MOUSE_BUTTON_LEFT + assert click_event.modifiers == 0 assert widget1.on_click.called +def test_ignores_right_click_on_widget(uimanager): + # GIVEN + widget1 = UIDummy() + widget1.on_click = Mock() + uimanager.add(widget1) + + # WHEN + with record_ui_events(widget1, "on_event", "on_click") as records: + uimanager.click(widget1.center_x, widget1.center_y, button=arcade.MOUSE_BUTTON_RIGHT) + + # THEN + records: List[UIEvent] + assert len(records) == 2 + assert isinstance(records[0], UIMousePressEvent) + assert isinstance(records[1], UIMouseReleaseEvent) + assert not widget1.on_click.called + + def test_click_on_widget_if_disabled(uimanager): # GIVEN widget1 = UIDummy()