diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 1f330b428..8ecb3a215 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from typing import Generator, Optional +from PIL import Image from typing_extensions import Self import arcade @@ -251,3 +252,7 @@ def resize(self, *, size: tuple[int, int], pixel_ratio: float) -> None: self.texture = self.ctx.texture(self.size_scaled, components=4) self.fbo = self.ctx.framebuffer(color_attachments=[self.texture]) self.fbo.clear() + + def to_image(self) -> Image.Image: + """Convert the surface to an PIL image""" + return self.ctx.get_framebuffer_image(self.fbo) diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 6d4abb5fd..dbb289eeb 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import ABC -from random import randint from typing import NamedTuple, Iterable, Optional, Union, TYPE_CHECKING, TypeVar, Tuple, List, Dict from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED @@ -22,7 +21,7 @@ from arcade.gui.nine_patch import NinePatchTexture from arcade.gui.property import Property, bind, ListProperty from arcade.gui.surface import Surface -from arcade.types import RGBA255, Color, AnchorPoint, AsFloat +from arcade.types import Color, AnchorPoint, AsFloat from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: @@ -389,7 +388,7 @@ def resize(self, *, width=None, height=None, anchor: Vec2 = AnchorPoint.CENTER): """ self.rect = self.rect.resize(width=width, height=height, anchor=anchor) - def with_border(self, *, width=2, color=arcade.color.GRAY) -> Self: + def with_border(self, *, width=2, color: Color | None = arcade.color.GRAY) -> Self: """Sets border properties Args: @@ -523,11 +522,9 @@ class UIInteractiveWidget(UIWidget): size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested size_hint_min: min width and height in pixel - size_hint_max: max width and height in pixel:param x: center x - of widget + size_hint_max: max width and height in pixel interaction_buttons: defines, which mouse buttons should trigger the interaction (default: left mouse button) - style: not used """ # States @@ -617,8 +614,9 @@ def on_click(self, event: UIOnClickEvent): class UIDummy(UIInteractiveWidget): - """Solid color widget used for testing & examples + """Solid color widget used for testing & examples. + Starts with a random color. It should not be subclassed for real-world usage. When clicked, it does the following: @@ -629,14 +627,13 @@ class UIDummy(UIInteractiveWidget): Args: x: x coordinate of bottom left y: y coordinate of bottom left - color: fill color for the widget width: width of widget height: height of widget size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested size_hint_min: min width and height in pixel size_hint_max: max width and height in pixel - style: not used + **kwargs: passed to UIWidget """ def __init__( @@ -659,25 +656,22 @@ def __init__( size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, + **kwargs, ) - self.color: RGBA255 = (randint(0, 255), randint(0, 255), randint(0, 255), 255) - self.border_color = arcade.color.BATTLESHIP_GREY - self.border_width = 0 + self.with_background(color=Color.random(a=255)) + self.with_border(color=arcade.color.BATTLESHIP_GREY, width=0) def on_click(self, event: UIOnClickEvent): """Prints the rect and changes the color""" print("UIDummy.rect:", self.rect) - self.color = Color.random(a=255) + self.with_background(color=Color.random(a=255)) def on_update(self, dt): """Update the border of the widget if hovered""" - self.border_width = 2 if self.hovered else 0 - self.border_color = arcade.color.WHITE if self.pressed else arcade.color.BATTLESHIP_GREY - - def do_render(self, surface: Surface): - """Render solid color""" - self.prepare_render(surface) - surface.clear(self.color) + self.with_border( + width=2 if self.hovered else 0, + color=arcade.color.WHITE if self.pressed else arcade.color.BATTLESHIP_GREY, + ) class UISpriteWidget(UIWidget): @@ -693,7 +687,6 @@ class UISpriteWidget(UIWidget): parent should be requested size_hint_min: min width and height in pixel size_hint_max: max width and height in pixel - style: not used """ def __init__( @@ -749,7 +742,6 @@ class UILayout(UIWidget): parent should be requested size_hint_min: min width and height in pixel size_hint_max: max width and height in pixel - style: not used """ @staticmethod @@ -809,7 +801,8 @@ class UISpace(UIWidget): y: y coordinate of bottom left width: width of widget height: height of widget - color: Color for widget area + color: Color for widget area, if None, it will be transparent + (this will set the background color) size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested size_hint_min: min width and height in pixel @@ -840,20 +833,13 @@ def __init__( size_hint_max=size_hint_max, **kwargs, ) - self._color = color + self.with_background(color=color) @property def color(self): - """Color of the widget""" - return self._color + """Color of the widget, alias for background color""" + return self._bg_color @color.setter def color(self, value): - self._color = value - self.trigger_render() - - def do_render(self, surface: Surface): - """Render the widget, mainly the background color""" - self.prepare_render(surface) - if self._color: - surface.clear(self._color) + self.with_background(color=value) diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 9a7c04d6c..50401f1bd 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -362,6 +362,8 @@ class UIInputText(UIWidget): around the caret. Arcade confirms that the field is active before allowing users to type, so it is okay to have multiple of these. + By default, a border is drawn around the input field. + Args: x: x position (default anchor is bottom-left). y: y position (default anchor is bottom-left). @@ -380,6 +382,9 @@ class UIInputText(UIWidget): is the same thing as a :py:class:`~arcade.gui.UITextArea`. caret_color: An RGBA or RGB color for the caret with each channel between 0 and 255, inclusive. + border_color: An RGBA or RGB color for the border with each + channel between 0 and 255, inclusive, can be None to remove border. + border_width: Width of the border in pixels. size_hint: A tuple of floats between 0 and 1 defining the amount of space of the parent should be requested. size_hint_min: Minimum size hint width and height in pixel. @@ -398,13 +403,15 @@ def __init__( x: float = 0, y: float = 0, width: float = 100, - height: float = 24, + height: float = 23, # required height for font size 12 + border width 1 text: str = "", font_name=("Arial",), font_size: float = 12, text_color: RGBOrA255 = arcade.color.WHITE, multiline=False, caret_color: RGBOrA255 = arcade.color.WHITE, + border_color: Color | None = arcade.color.WHITE, + border_width: int = 2, size_hint=None, size_hint_min=None, size_hint_max=None, @@ -421,6 +428,8 @@ def __init__( **kwargs, ) + self.with_border(color=border_color, width=border_width) + self._active = False self._text_color = Color.from_iterable(text_color) @@ -467,7 +476,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: if not self._active and isinstance(event, UIMousePressEvent): if self.rect.point_in_rect(event.pos): self.activate() - return EVENT_HANDLED + # return unhandled to allow other widgets to deactivate + return EVENT_UNHANDLED # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): @@ -477,6 +487,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: 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