From fb45f9424dc2a24bd3c091fe9440acd97ec9e8b0 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:27:30 -0400 Subject: [PATCH 1/5] Encapsulate relative position logic in UITextInput * Add UIWidget._event_pos_relative_to_self * Add UITextInput._event_pos_relative_to_self * Use new method to clean up UITextInput.on_event --- arcade/gui/widgets/__init__.py | 28 +++++++++++++++++++++++++++- arcade/gui/widgets/text.py | 18 ++++++++++++------ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index dbb289eeb..6d05d19f9 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -1,7 +1,18 @@ from __future__ import annotations from abc import ABC -from typing import NamedTuple, Iterable, Optional, Union, TYPE_CHECKING, TypeVar, Tuple, List, Dict +from typing import ( + NamedTuple, + Iterable, + Optional, + Union, + TYPE_CHECKING, + TypeVar, + Tuple, + List, + Dict, + Type, +) from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED from pyglet.math import Vec2 @@ -17,6 +28,7 @@ UIMouseReleaseEvent, UIOnClickEvent, UIOnUpdateEvent, + UIMouseEvent, ) from arcade.gui.nine_patch import NinePatchTexture from arcade.gui.property import Property, bind, ListProperty @@ -167,6 +179,20 @@ def on_update(self, dt): """Custom logic which will be triggered.""" pass + def _event_pos_relative_to_self(self, mouse_event: UIMouseEvent) -> Vec2 | None: + """Gets coords relative to bottom left if inside the widget. + + Args: + mouse_event: Any :py:class:`UIMouseEvent`. + Returns: + ``None`` if outside the widget or coords relative to + the widget's bottom left. + """ + pos = mouse_event.pos + if not self.rect.point_in_rect(pos): + return None + return Vec2(pos[0] - self.left, pos[1] - self.bottom) + def on_event(self, event: UIEvent) -> Optional[bool]: """Passes :class:`UIEvent` s through the widget tree.""" # UpdateEvents are past to the first invisible widget diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 50401f1bd..be1eba4de 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import Optional +from typing import Final, Optional import pyglet from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED +from pyglet.math import Vec2 from pyglet.text.caret import Caret from pyglet.text.document import AbstractDocument from typing_extensions import override @@ -396,6 +397,7 @@ class UIInputText(UIWidget): # Move layout one pixel into the scissor box so the caret is also shown at # position 0. LAYOUT_OFFSET = 1 + LAYOUT_OFFSET_VEC2: Final[Vec2] = Vec2(0.0, LAYOUT_OFFSET) def __init__( self, @@ -467,6 +469,12 @@ def on_update(self, dt): self._blink_state = current_state self.trigger_full_render() + @override + def _event_pos_relative_to_self(self, mouse_event: UIMouseEvent) -> Vec2 | None: + if result := super()._event_pos_relative_to_self(mouse_event): + return result - self.LAYOUT_OFFSET_VEC2 + return result + @override def on_event(self, event: UIEvent) -> Optional[bool]: """Handle events for the text input field. @@ -482,8 +490,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): if self.rect.point_in_rect(event.pos): - x = int(event.x - self.left - self.LAYOUT_OFFSET) - y = int(event.y - self.bottom) + x, y = map(int, self._event_pos_relative_to_self(event)) self.caret.on_mouse_press(x, y, event.button, event.modifiers) else: self.deactivate() @@ -503,9 +510,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_text_motion_select(event.selection) self.trigger_full_render() - if isinstance(event, UIMouseEvent) and self.rect.point_in_rect(event.pos): - x = int(event.x - self.left - self.LAYOUT_OFFSET) - y = int(event.y - self.bottom) + if isinstance(event, UIMouseEvent) and (xy := self._event_pos_relative_to_self(event)): + x, y = map(int, xy) if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( x, y, event.dx, event.dy, event.buttons, event.modifiers From 9222a53408e3d98590a6ac2fc3088506b2535eb3 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:43:15 -0400 Subject: [PATCH 2/5] Refactor UITextInput.on_event's mouse press handling * Use self._event_pos_relative_to_self * Restructure it to put the type check * Add comments explaining when / why things are handled --- arcade/gui/widgets/text.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index be1eba4de..5faa0ed49 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -480,24 +480,25 @@ def on_event(self, event: UIEvent) -> Optional[bool]: """Handle events for the text input field. Text input is only active when the user clicks on the input field.""" - # If not active, check to activate, return - if not self._active and isinstance(event, UIMousePressEvent): - if self.rect.point_in_rect(event.pos): + # Handle mouse presses to active, deactivate, or move the caret. All other + # mouse events are handled in the if self._active block after this one. + if isinstance(event, UIMousePressEvent): + inside_xy = self._event_pos_relative_to_self(event) + if self._active: + if inside_xy: + x, y = map(int, inside_xy) + self.caret.on_mouse_press(x, y, event.button, event.modifiers) + else: + self.deactivate() + # return unhandled to allow other widgets to activate + return EVENT_UNHANDLED + + elif not self._active and inside_xy: self.activate() # return unhandled to allow other widgets to deactivate return EVENT_UNHANDLED - # If active check to deactivate - if self._active and isinstance(event, UIMousePressEvent): - if self.rect.point_in_rect(event.pos): - x, y = map(int, self._event_pos_relative_to_self(event)) - self.caret.on_mouse_press(x, y, event.button, event.modifiers) - else: - self.deactivate() - # return unhandled to allow other widgets to activate - return EVENT_UNHANDLED - - # If active pass all non press events to caret + # If active, then pass all supported non-press events to caret if self._active: # Act on events if active if isinstance(event, UITextInputEvent): From c0a425469c74d86e5bcee5f4b45083d86a44e19b Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:44:56 -0400 Subject: [PATCH 3/5] Phrasing and doc tweaks * Use inside_xy in the lower block as well * Improve UITextInput.on_event docstring + comments --- arcade/gui/widgets/text.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 5faa0ed49..62f9b2ed5 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -479,9 +479,19 @@ def _event_pos_relative_to_self(self, mouse_event: UIMouseEvent) -> Vec2 | None: def on_event(self, event: UIEvent) -> Optional[bool]: """Handle events for the text input field. - Text input is only active when the user clicks on the input field.""" - # Handle mouse presses to active, deactivate, or move the caret. All other - # mouse events are handled in the if self._active block after this one. + Text input and other editing events only work after a user clicks + inside the field (:py:class:`~arcade.gui.events.UIMousePress`). + Dragging to select works as expected because it is handled by a separate + event class (:py:class:`~arcade.gui.events.UIMouseEventDrag`). + + Args: + event: The UI event to be handled here or in a superclass. + Returns: + ``True`` if the event was handled. + """ + + # Handle mouse presses to activate or deactivate the caret. All other mouse + # events, including dragging and scrolling, are handled in the next if block. if isinstance(event, UIMousePressEvent): inside_xy = self._event_pos_relative_to_self(event) if self._active: @@ -498,7 +508,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: # return unhandled to allow other widgets to deactivate return EVENT_UNHANDLED - # If active, then pass all supported non-press events to caret + # When active, handle any supported non-press events by passing them + # to the caret object. if self._active: # Act on events if active if isinstance(event, UITextInputEvent): @@ -511,8 +522,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_text_motion_select(event.selection) self.trigger_full_render() - if isinstance(event, UIMouseEvent) and (xy := self._event_pos_relative_to_self(event)): - x, y = map(int, xy) + if isinstance(event, UIMouseEvent) and (inside_xy := self._event_pos_relative_to_self(event)): + x, y = map(int, inside_xy) if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( x, y, event.dx, event.dy, event.buttons, event.modifiers From 2e19aa043e70df93558c0ddc5dd5bb8363cd7e47 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:26:49 -0400 Subject: [PATCH 4/5] Formatting --- arcade/gui/widgets/text.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 62f9b2ed5..dbcf7139a 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -522,7 +522,9 @@ def on_event(self, event: UIEvent) -> Optional[bool]: self.caret.on_text_motion_select(event.selection) self.trigger_full_render() - if isinstance(event, UIMouseEvent) and (inside_xy := self._event_pos_relative_to_self(event)): + if isinstance(event, UIMouseEvent) and ( + inside_xy := self._event_pos_relative_to_self(event) + ): x, y = map(int, inside_xy) if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( From 302159b21455085662f7d599ae02208842b83911 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Mon, 29 Jul 2024 06:41:47 -0400 Subject: [PATCH 5/5] Remove unused import --- arcade/gui/widgets/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 6d05d19f9..d5c46be44 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -11,7 +11,6 @@ Tuple, List, Dict, - Type, ) from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED