"""
pygame-menu
https://github.com/ppizarror/pygame-menu
IMAGE
Image widget class, adds a simple image.
"""
from __future__ import annotations
__all__ = ["Image", "ImageManager"]
from abc import ABC
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any
import pygame
from pygame_menu._types import (
CallbackType,
EventVectorType,
NumberInstance,
NumberType,
Tuple2NumberType,
Vector2NumberType,
)
from pygame_menu.baseimage import BaseImage
from pygame_menu.utils import assert_vector
from pygame_menu.widgets.core.widget import AbstractWidgetManager, Widget
if TYPE_CHECKING:
from collections.abc import Callable
import pygame_menu
[docs]
class Image(Widget):
"""
Image widget.
.. note::
Image accepts all transformations.
:param image_path: Path of the image, BytesIO object, a pygame.Surface, or :py:class:`pygame_menu.baseimage.BaseImage` object. If :py:class:`pygame_menu.baseimage.BaseImage` object is provided drawing mode is not considered
:param image_id: Image ID
:param angle: Angle of the image in degrees (clockwise)
:param onselect: Function when selecting the widget
:param scale: Scale of the image on x-axis and y-axis (x, y) in px
:param scale_smooth: Scale is smoothed
"""
_image: BaseImage
def __init__(
self,
image_path: str | Path | BaseImage | BytesIO | pygame.Surface,
angle: NumberType = 0,
image_id: str = "",
onselect: CallbackType = None,
scale: Tuple2NumberType = (1, 1),
scale_smooth: bool = True,
) -> None:
assert isinstance(image_path, (str, Path, BaseImage, BytesIO, pygame.Surface))
assert isinstance(image_id, str)
assert isinstance(angle, NumberInstance)
assert isinstance(scale_smooth, bool)
assert_vector(scale, 2)
super().__init__(onselect=onselect, widget_id=image_id)
if isinstance(image_path, BaseImage):
self._image = image_path
else:
self._image = BaseImage(image_path)
self._image.rotate(angle)
self._image.scale(scale[0], scale[1], smooth=scale_smooth)
def set_title(self, title: str, *args) -> Image:
return self
[docs]
def get_image(self) -> BaseImage:
"""
Gets the :py:class:`pygame_menu.baseimage.BaseImage` object from widget.
:return: Widget image
"""
return self._image
[docs]
def get_angle(self) -> NumberType:
"""
Return the image angle.
:return: Angle in degrees
"""
return self._image.get_angle()
[docs]
def set_image(self, image: BaseImage) -> None:
"""
Set the :py:class:`pygame_menu.baseimage.BaseImage` object from widget.
:param image: Image object
"""
self._image = image
self._surface = None
self._render()
def _apply_font(self) -> None:
pass
def _update_surface(self) -> Image:
"""
Updates surface and renders.
:return: Self reference
"""
self._surface = None
self._render()
return self
[docs]
def scale(
self,
width: NumberType,
height: NumberType,
smooth: bool = False,
render: bool = True,
) -> Image:
self._image.scale(width, height, smooth)
return self._update_surface()
[docs]
def resize(
self,
width: NumberType,
height: NumberType,
smooth: bool = False,
render: bool = True,
) -> Image:
self._image.resize(width, height, smooth)
self._surface = None
return self._update_surface()
[docs]
def set_max_width(
self,
width: NumberType | None,
scale_height: NumberType = False,
smooth: bool = True,
render: bool = True,
) -> Image:
if width is not None and self._image.get_width() > width:
sx = width / self._image.get_width()
height = self._image.get_height()
if scale_height:
height *= sx
self._image.resize(width, height, smooth)
return self._update_surface()
return self
[docs]
def set_max_height(
self,
height: NumberType | None,
scale_width: NumberType = False,
smooth: bool = True,
render: bool = True,
) -> Image:
if height is not None and self._image.get_height() > height:
sy = height / self._image.get_height()
width = self._image.get_width()
if scale_width:
width *= sy
self._image.resize(width, height, smooth)
return self._update_surface()
return self
[docs]
def rotate(self, angle: NumberType, render: bool = True) -> Image:
self._image.rotate(angle)
return self._update_surface()
[docs]
def flip(self, x: bool, y: bool, render: bool = True) -> Image:
assert isinstance(x, bool)
assert isinstance(y, bool)
self._flip = (x, y)
if x or y:
self._image.flip(x, y)
return self._update_surface()
return self
def _draw(self, surface: pygame.Surface) -> None:
surface.blit(self._surface, self._rect.topleft)
def _render(self) -> bool | None:
if self._surface is not None:
return True
self._surface = self._image.get_surface(new=False)
self._rect.width, self._rect.height = self._surface.get_size()
if not self._render_hash_changed(self._visible):
return True
self.force_menu_surface_update()
return None
def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
for event in events:
if self._check_mouseover(event):
break
return False
class ImageManager(AbstractWidgetManager, ABC):
"""
Image manager.
"""
def image(
self,
image_path: str | Path | pygame_menu.BaseImage | BytesIO | pygame.Surface,
angle: NumberType = 0,
image_id: str = "",
onselect: Callable[[bool, Widget, pygame_menu.Menu], Any] | None = None,
scale: Vector2NumberType = (1, 1),
scale_smooth: bool = True,
selectable: bool = False,
**kwargs,
) -> pygame_menu.widgets.Image:
"""
Add a simple image to the Menu.
If ``onselect`` is defined, the callback is executed as follows, where
``selected`` is a boolean representing the selected status:
.. code-block:: python
onselect(selected, widget, menu)
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color
- ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect. Applied only if ``selectable`` is ``True``
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behavior apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
:param image_path: Path of the image (file), a pygame.Surface, or a BaseImage object. If BaseImage object is provided the angle and scale are ignored
:param angle: Angle of the image in degrees (clockwise)
:param image_id: ID of the image
:param onselect: Callback executed when selecting the widget; only executed if ``selectable`` is ``True``
:param scale: Scale of the image on x-axis and y-axis (x, y)
:param scale_smooth: Scale is smoothed
:param selectable: Image accepts user selection
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.Image`
"""
assert isinstance(image_path, (str, Path, BaseImage, BytesIO, pygame.Surface))
assert isinstance(selectable, bool)
# Remove invalid keys from kwargs
for key in list(kwargs.keys()):
if key not in (
"align",
"background_color",
"background_inflate",
"border_color",
"border_inflate",
"border_width",
"cursor",
"margin",
"padding",
"selection_color",
"selection_effect",
"border_position",
"float",
"float_origin_position",
"shadow_color",
"shadow_radius",
"shadow_type",
"shadow_width",
):
kwargs.pop(key, None)
# Filter widget attributes to avoid passing them to the callbacks
attributes = self._filter_widget_attributes(kwargs)
widget = Image(
angle=angle,
image_id=image_id,
image_path=image_path,
onselect=onselect,
scale=scale,
scale_smooth=scale_smooth,
)
widget.is_selectable = selectable
self._check_kwargs(kwargs)
self._configure_widget(widget=widget, **attributes)
self._append_widget(widget)
return widget