from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import pygame
from pygame import SRCALPHA
from nqp.base_classes.animation import Animation
from nqp.base_classes.image import Image
from nqp.base_classes.ui_element import UIElement
from nqp.core.constants import GAP_SIZE, TextRelativePosition
from nqp.core.utility import clamp
from nqp.ui_elements.generic.fancy_font import FancyFont
from nqp.ui_elements.generic.font import Font
if TYPE_CHECKING:
from typing import Optional, Tuple, Union
from nqp.core.game import Game
__all__ = ["UIFrame"]
[docs]class UIFrame(UIElement):
"""
A complex ui element. Works as an extension of UIElement to offer more functionality within a helpful wrapper.
"""
[docs] def __init__(
self,
game: Game,
pos: pygame.Vector2,
font: None | Font | FancyFont = None,
is_selectable: bool = False,
max_width: Optional[int] = None,
max_height: Optional[int] = None,
image: None | Image | Animation = None,
text_relative_position: TextRelativePosition = None,
tooltip_key: str | None = None,
):
super().__init__(game, pos, is_selectable, tooltip_key)
self._image: Optional[Union[Image, Animation]] = image
self._font: Optional[Union[Font, FancyFont]] = font
self._max_width: Optional[int] = max_width
self._max_height: Optional[int] = max_height
self._text_relative_position: Optional[TextRelativePosition] = text_relative_position
self._override_font_attrs()
self._recalculate_size()
self.update(0) # update to prevent the flash of all the text showing at the start
self._rebuild_surface()
[docs] def update(self, delta_time: float):
super().update(delta_time)
if self.is_active:
# FancyFont changes each frame so needs redrawing
if isinstance(self._font, FancyFont):
self._font.update(delta_time)
# N.B. Animation updates centrally so doesnt need to be updated here
[docs] def draw(self, surface: pygame.Surface):
super().draw(surface)
is_dirty = False
redraw_font = False
if self.is_active:
# FancyFont changes each frame so needs redrawing
if isinstance(self._font, FancyFont):
is_dirty = True
redraw_font = True
# Animation changes each frame
if isinstance(self._image, Animation):
is_dirty = True
# rebuild surface first
if is_dirty:
self._rebuild_surface()
if redraw_font:
self._font.draw(self.surface)
def _recalculate_size(self):
image = self._image
text_relative_position = self._text_relative_position
font = self._font
# logging.info("Recalculating size for ui_frame.")
width = image_width = 0
height = image_height = 0
if image is not None:
image_width = image.surface.get_width()
image_height = image.surface.get_height()
width += image_width
height += image_height
# logging.debug(f"Dimensions for image: ({image_width}, {image_height})")
if font is not None:
# logging.debug(f"Dimensions for font: ({font.width}, {font.height})")
if text_relative_position is not None:
if text_relative_position in (TextRelativePosition.ABOVE_IMAGE, TextRelativePosition.BELOW_IMAGE):
height += font.height + GAP_SIZE
width = max(font.width, image_width)
else:
width += font.width + GAP_SIZE
else:
width += font.width + GAP_SIZE
# check which is taller, font or image
if image is not None:
height = max(image_height, font.height)
else:
# no image so take font height
height += font.height
# logging.debug(f"Size of gap: {GAP_SIZE}")
# respect max height
if self._max_height is not None:
height = min(height, self._max_height)
# respect max width
if self._max_width is not None:
width = min(width, self._max_width)
# if text_relative_position:
# logging.debug(f"Recalculated size ({width}, {height}) for ui_frame with {text_relative_position.name}.")
# else:
# logging.debug(f"Recalculated size ({width}, {height}) for ui_frame with no text relative position")
self.size = pygame.Vector2(width, height)
def _rebuild_surface(self):
self.surface = pygame.Surface(self.size, SRCALPHA)
surface = self.surface
image = self._image
font = self._font
text_relative_position = self._text_relative_position
image_position: pygame.Vector2 = pygame.Vector2(0, 0)
text_position: pygame.Vector2 = pygame.Vector2(0, 0)
if text_relative_position is TextRelativePosition.ABOVE_IMAGE:
image_position.y += font.height
elif text_relative_position is TextRelativePosition.BELOW_IMAGE:
text_position.y += image.height
elif text_relative_position is TextRelativePosition.RIGHT_OF_IMAGE:
text_position.x += image.height
elif text_relative_position is TextRelativePosition.LEFT_OF_IMAGE:
image_position.x += font.width
# draw image
if image is not None:
surface.blit(image.surface, image_position)
# draw text
if font is not None:
# Font can be drawn once (FancyFont needs constant redrawing so is handled in the draw method)
if isinstance(font, Font):
font.pos.x += text_position.x
font.pos.y += text_position.y
font.draw(surface)
def _override_font_attrs(self):
"""
Force the font to use the frame's pos and max sizes.
"""
image = self._image
font = self._font
if not font:
return
# offset for image, if there is one
if image:
image_width = image.surface.get_width()
x = image_width + GAP_SIZE
else:
image_width = 0
x = 0
# update font pos (remember, this is relative to the frame)
y = font.line_height // 2
font.pos = pygame.Vector2(x, y)
# update font size
if self._max_width is not None:
new_line_width = max(self._max_width - image_width, font.line_width)
else:
self._recalculate_size()
new_line_width = max(self.width - image_width, font.line_width)
font.line_width = new_line_width
# FancyFont needs to refresh
if isinstance(font, FancyFont):
font.refresh()
pass
[docs] def set_text(self, text: str):
"""
Update the font text.
"""
font = self._font
text = str(text)
if isinstance(font, FancyFont):
font.raw_text = text
font.refresh()
elif isinstance(font, Font):
font.text = text
self._rebuild_surface()
[docs] def add_tier_background(self, tier: int):
"""
Add a background to the frame, based on the tier given. Tiers can be 1-4.
"""
# TODO - this probably isnt approprioate here anymore and should be on a tailored uielement
tier_colours = {
1: (141, 148, 150),
2: (30, 117, 54),
3: (30, 63, 117),
4: (85, 30, 117),
}
# normalise value
tier = clamp(tier, 1, 4)
# create background and blit image onto it
bg = pygame.Surface(self._image.surface.get_size())
bg.fill(tier_colours[tier])
bg.blit(self._image.surface, (0, 0))
self._image = bg
self._rebuild_surface()
[docs] def pause_animation(self):
"""
Pause the animation, if there is one
"""
if isinstance(self._image, Animation):
self._image.pause()
[docs] def play_animation(self):
"""
Play the animation, if there is one
"""
if isinstance(self._image, Animation):
self._image.play()
[docs] def reset_animation(self):
"""
Reset the animation, if there is one
"""
if isinstance(self._image, Animation):
self._image.reset()
[docs] def stop_animation(self):
"""
Stop the animation, if there is one
"""
if isinstance(self._image, Animation):
self._image.stop()