Skip to content

Commit

Permalink
switch to pygame.font for rendering text
Browse files Browse the repository at this point in the history
should enable some arabic font rendering eventually with a bit more hacking
  • Loading branch information
MyreMylar committed Feb 14, 2024
1 parent 02e73cc commit 681f121
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 121 deletions.
29 changes: 5 additions & 24 deletions pygame_gui/core/gui_font_freetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self, file: Optional[FileArg], size: Union[int, float],
self.__internal_font: Font = Font(file, size, resolution=72)
self.__internal_font.pad = True
self.__internal_font.origin = True
self.__internal_font.kerning = True

self.__underline = False
self.__underline_adjustment = 0.0
Expand Down Expand Up @@ -50,17 +51,10 @@ def underline_adjustment(self, value: float):
def get_point_size(self):
return self.point_size

def get_font(self) -> Font:
return self.__internal_font

def get_text_height(self, text: str) -> int:
pass

def get_text_width(self, text: str) -> int:
pass

def get_rect(self, text: str) -> Rect:
return self.__internal_font.get_rect(text)
supposed_rect = self.__internal_font.get_rect(text)
text_surface, text_rect = self.__internal_font.render(text, pygame.Color("white"))
return pygame.Rect(supposed_rect.topleft, text_surface.get_size())

def get_metrics(self, text: str):
return self.__internal_font.get_metrics(text)
Expand Down Expand Up @@ -88,18 +82,5 @@ def get_padding_height(self):
# that doesn't drop below the base line (no y's, g's, p's etc)
# but also don't want it to flicker on and off. Base-line
# centering is the default for chunks on a single style row.
padding_state = self.__internal_font.pad

self.__internal_font.pad = False
no_pad_origin = self.__internal_font.get_rect('A').y

self.__internal_font.pad = True
pad_origin = self.__internal_font.get_rect('A').y

self.__internal_font.pad = padding_state
return pad_origin - no_pad_origin

def remove_font_pad_and_origin(self):
self.__internal_font.pad = False
self.__internal_font.origin = False

return -self.__internal_font.get_sized_descender(self.point_size)
98 changes: 98 additions & 0 deletions pygame_gui/core/gui_font_pygame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import pygame

from pygame_gui.core.interfaces.gui_font_interface import IGUIFontInterface
from pygame.font import Font
from typing import Union, IO, Optional, Dict, Tuple
from os import PathLike
from pygame import Color, Surface, Rect

AnyPath = Union[str, bytes, PathLike]
FileArg = Union[AnyPath, IO]


class GUIFontPygame(IGUIFontInterface):

def __init__(self, file: Optional[FileArg], size: Union[int, float],
force_style: bool = False, style: Optional[Dict[str, bool]] = None):
self.__internal_font: Font = Font(file, size) # no resolution option for pygame font?

self.__internal_font.set_point_size(size)
self.pad = True
self.origin = True
self.__underline = False
self.__underline_adjustment = 0.0

self.point_size = size
self.antialiased = True

if style is not None:
self.antialiased = style['antialiased']

if force_style:
self.__internal_font.bold = style['bold']
self.__internal_font.italic = style['italic']

Check warning on line 33 in pygame_gui/core/gui_font_pygame.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/gui_font_pygame.py#L32-L33

Added lines #L32 - L33 were not covered by tests

def size(self, text: str):
return self.__internal_font.size(text)

Check warning on line 36 in pygame_gui/core/gui_font_pygame.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/gui_font_pygame.py#L36

Added line #L36 was not covered by tests

@property
def underline(self) -> bool:
return self.__internal_font.underline

@underline.setter
def underline(self, value: bool):
self.__internal_font.underline = value

@property
def underline_adjustment(self) -> float:
# underline adjustment is missing in pygame.font. Would need to be added to SDL ttf
return self.__underline_adjustment

@underline_adjustment.setter
def underline_adjustment(self, value: float):
self.__underline_adjustment = value

def get_point_size(self):
return self.point_size

def get_rect(self, text: str) -> Rect:
# only way to get accurate font layout data with kerning is to render it ourselves it seems
text_surface = self.__internal_font.render(text, self.antialiased, pygame.Color("white"))
return pygame.Rect((0, self.__internal_font.get_ascent()), text_surface.get_size())

def get_metrics(self, text: str):
# this may need to be broken down further in the wrapper
return self.__internal_font.metrics(text)

def render_premul(self, text: str, text_color: Color) -> Surface:
text_surface = self.__internal_font.render(text, self.antialiased, text_color)
text_surface = text_surface.convert_alpha()
if text_surface.get_width() > 0 and text_surface.get_height() > 0:
text_surface = text_surface.premul_alpha()
return text_surface

def render_premul_to(self, text: str, text_colour: Color,
surf_size: Tuple[int, int], surf_position: Tuple[int, int]) -> Surface:
text_surface = pygame.Surface(surf_size, depth=32, flags=pygame.SRCALPHA)
text_surface.fill((0, 0, 0, 0))
temp_surf = self.__internal_font.render(text, self.antialiased, text_colour)
temp_surf = temp_surf.convert_alpha()
if temp_surf.get_width() > 0 and temp_surf.get_height() > 0:
temp_surf = temp_surf.premul_alpha()
text_surface.blit(temp_surf, (surf_position[0], surf_position[1]-self.__internal_font.get_ascent()),
special_flags=pygame.BLEND_PREMULTIPLIED)
return text_surface

def get_padding_height(self):
# 'font padding' this determines the amount of padding that
# font.pad adds to the top of text excluding
# any padding added to make glyphs even - this is useful
# for 'base-line centering' when we want to center text
# that doesn't drop below the base line (no y's, g's, p's etc)
# but also don't want it to flicker on and off. Base-line
# centering is the default for chunks on a single style row.

descender = self.__internal_font.get_descent()
return -descender + 1


22 changes: 0 additions & 22 deletions pygame_gui/core/interfaces/gui_font_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,6 @@ def render_premul_to(self, text: str, text_colour: Color, surf_size: Tuple[int,
:return:
"""

@abstractmethod
def get_text_width(self, text: str) -> int:
"""
Returns the width of text drawn by this font in pixels
:param text:
:return: The pixel width of the text.
"""

@abstractmethod
def get_text_height(self, text: str) -> int:
"""
Returns the height of text drawn by this font in pixels
:param text:
:return: The pixel height of the text.
"""

@abstractmethod
def get_rect(self, text: str) -> Rect:
"""
Expand Down Expand Up @@ -103,9 +87,3 @@ def underline_adjustment(self, value: float):
:return:
"""

@abstractmethod
def remove_font_pad_and_origin(self):
"""
:return:
"""
29 changes: 22 additions & 7 deletions pygame_gui/core/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from pygame_gui.core.interfaces import IUIManagerInterface, IGUIFontInterface
from pygame_gui.core.gui_font_freetype import GUIFontFreetype
from pygame_gui.core.gui_font_pygame import GUIFontPygame


__default_manager = None # type: Optional[IUIManagerInterface]
Expand Down Expand Up @@ -380,7 +381,9 @@ def __init__(self,
self.style = style
self.location = location[0]
self.force_style = location[1]
self.loaded_font = None # type: Union[GUIFontFreetype, None]
self.loaded_font = None # type: Union[IGUIFontInterface, None]

self.font_type_to_use = "pygame"

def load(self):
"""
Expand All @@ -392,25 +395,37 @@ def load(self):
error = None
if isinstance(self.location, PackageResource):
try:
self.loaded_font = GUIFontFreetype(
io.BytesIO((resources.files(self.location.package) /
self.location.resource).read_bytes()),
self.size, self.force_style, self.style)
if self.font_type_to_use == "freetype":
self.loaded_font = GUIFontFreetype(

Check warning on line 399 in pygame_gui/core/utility.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/utility.py#L399

Added line #L399 was not covered by tests
io.BytesIO((resources.files(self.location.package) /
self.location.resource).read_bytes()),
self.size, self.force_style, self.style)
elif self.font_type_to_use == "pygame":
self.loaded_font = GUIFontPygame(
io.BytesIO((resources.files(self.location.package) /
self.location.resource).read_bytes()),
self.size, self.force_style, self.style)
except (pygame.error, FileNotFoundError, OSError):
error = FileNotFoundError('Unable to load resource with path: ' +
str(self.location))

elif isinstance(self.location, str):
try:
self.loaded_font = GUIFontFreetype(self.location, self.size, self.force_style, self.style)
if self.font_type_to_use == "freetype":
self.loaded_font = GUIFontFreetype(self.location, self.size, self.force_style, self.style)

Check warning on line 415 in pygame_gui/core/utility.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/utility.py#L415

Added line #L415 was not covered by tests
elif self.font_type_to_use == "pygame":
self.loaded_font = GUIFontPygame(self.location, self.size, self.force_style, self.style)
except (pygame.error, FileNotFoundError, OSError):
error = FileNotFoundError('Unable to load resource with path: ' +
str(self.location))

elif isinstance(self.location, bytes):
try:
file_obj = io.BytesIO(base64.standard_b64decode(self.location))
self.loaded_font = GUIFontFreetype(file_obj, self.size, self.force_style, self.style)
if self.font_type_to_use == "freetype":
self.loaded_font = GUIFontFreetype(file_obj, self.size, self.force_style, self.style)
elif self.font_type_to_use == "pygame":
self.loaded_font = GUIFontPygame(file_obj, self.size, self.force_style, self.style)

Check warning on line 428 in pygame_gui/core/utility.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/core/utility.py#L425-L428

Added lines #L425 - L428 were not covered by tests
except (pygame.error, FileNotFoundError, OSError):
error = FileNotFoundError('Unable to load resource with path: ' +
str(self.location))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def test_apply_colour_to_surface(self, _init_pygame, default_ui_manager: UIManag
after_application_colour = test_surface_2.get_at((30, 0))
assert after_application_colour == pygame.Color(255, 255, 255, 255)

def test_rebuild_images_and_text(self, _init_pygame, default_ui_manager: UIManager):
def test_rebuild_images_and_text(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
shape = DrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'doop doop',
'font': default_ui_manager.get_theme().get_font([]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class TestEllipseDrawableShape:
def test_creation(self, _init_pygame, default_ui_manager: UIManager):
def test_creation(self, _init_pygame, _display_surface_return_none, default_ui_manager: UIManager):
EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.get_theme().get_font([]),
Expand All @@ -21,7 +21,8 @@ def test_creation(self, _init_pygame, default_ui_manager: UIManager):
'text_vert_alignment': 'center'},
states=['normal'], manager=default_ui_manager)

def test_full_rebuild_on_size_change_negative_values(self, _init_pygame, default_ui_manager: UIManager):
def test_full_rebuild_on_size_change_negative_values(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -36,7 +37,8 @@ def test_full_rebuild_on_size_change_negative_values(self, _init_pygame, default
states=['normal'], manager=default_ui_manager)
shape.full_rebuild_on_size_change()

def test_full_rebuild_on_size_change_large(self, _init_pygame, default_ui_manager: UIManager):
def test_full_rebuild_on_size_change_large(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 25, 25),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -51,7 +53,8 @@ def test_full_rebuild_on_size_change_large(self, _init_pygame, default_ui_manage
states=['normal'], manager=default_ui_manager)
shape.full_rebuild_on_size_change()

def test_full_rebuild_on_size_change_large_shadow(self, _init_pygame, default_ui_manager: UIManager):
def test_full_rebuild_on_size_change_large_shadow(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 2, 2),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -66,7 +69,8 @@ def test_full_rebuild_on_size_change_large_shadow(self, _init_pygame, default_ui
states=['normal'], manager=default_ui_manager)
shape.full_rebuild_on_size_change()

def test_collide_point(self, _init_pygame, default_ui_manager: UIManager):
def test_collide_point(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -81,7 +85,8 @@ def test_collide_point(self, _init_pygame, default_ui_manager: UIManager):
states=['normal'], manager=default_ui_manager)
assert shape.collide_point((50, 50)) is True

def test_set_position(self, _init_pygame, default_ui_manager: UIManager):
def test_set_position(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -96,7 +101,8 @@ def test_set_position(self, _init_pygame, default_ui_manager: UIManager):
states=['normal'], manager=default_ui_manager)
shape.set_position((50, 50))

def test_set_dimensions(self, _init_pygame, default_ui_manager: UIManager):
def test_set_dimensions(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
shape = EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.ui_theme.get_font([]),
Expand All @@ -113,7 +119,8 @@ def test_set_dimensions(self, _init_pygame, default_ui_manager: UIManager):

assert not shape.set_dimensions(shape.containing_rect.size)

def test_creation_with_gradients(self, _init_pygame, default_ui_manager: UIManager):
def test_creation_with_gradients(self, _init_pygame, _display_surface_return_none,
default_ui_manager: UIManager):
EllipseDrawableShape(containing_rect=pygame.Rect(0, 0, 100, 100),
theming_parameters={'text': 'test',
'font': default_ui_manager.get_theme().get_font([]),
Expand Down
Loading

0 comments on commit 681f121

Please sign in to comment.