diff --git a/src/doodles/__init__.py b/src/doodles/__init__.py index 227f488..82114e2 100644 --- a/src/doodles/__init__.py +++ b/src/doodles/__init__.py @@ -2,5 +2,6 @@ from .doodles import Doodle, Group from .lines import Line from .shapes import Circle, Rectangle from .color import Color +from .text import Text -__all__ = [Doodle, Group, Line, Circle, Rectangle, Color] +__all__ = [Doodle, Group, Line, Circle, Rectangle, Color, Text] diff --git a/src/doodles/examples/words.py b/src/doodles/examples/words.py new file mode 100644 index 0000000..9754178 --- /dev/null +++ b/src/doodles/examples/words.py @@ -0,0 +1,42 @@ +import random +import itertools +from doodles import Group, Circle, Color, Text + +# TODO: depending on system these fonts often do not have all the +# necessary characters, find 3 widely available fonts that do +Text.make_font("small", 16, "mono") +Text.make_font("medium", 24, "copperplate") +Text.make_font("large", 48, "papyrus") +print(Text._fonts) + +# Via ChatGPT +hello_world = [ + "Hello, World!", # English + "Hola, Mundo!", # Spanish + "Bonjour, Monde!", # French + "Hallo, Welt!", # German + "Ciao, Mondo!", # Italian + "こんにちは、世界!", # Japanese + "안녕하세요, 세계!", # Korean + "Привет, мир!", # Russian + "Olá, Mundo!", # Portuguese + "नमस्ते, दुनिया!", # Hindi + "Merhaba, Dünya!", # Turkish + "Salam, Dünya!", # Azerbaijani + "Hej, Världen!", # Swedish + "Hei, Maailma!", # Finnish + "שלום, עולם!", # Hebrew + "Szia, Világ!", # Hungarian + "Zdravo, Svijete!", # Bosnian + "Sawubona, Mhlaba!", # Zulu + "Marhaba, Alalam!", # Arabic + "你好,世界!", # Mandarin Chinese +] + + +def create(): + # use a generator to include each one 3x + for greeting in itertools.chain.from_iterable(itertools.repeat(hello_world, 3)): + Text().random().font( + random.choice(("small", "medium", "large")) + ).text(greeting) diff --git a/src/doodles/text.py b/src/doodles/text.py new file mode 100644 index 0000000..50bb9c0 --- /dev/null +++ b/src/doodles/text.py @@ -0,0 +1,85 @@ +import pygame +from .doodles import Doodle + +# TOOD: make configurable +DEFAULT_FONT_SIZE = 24 + +class Text(Doodle): + # Having each bit of text on the screen load a separate copy + # of its font would be wasteful, since the most common case would + # be for most text to use the same font. + # + # The solution here is to use a class attribute, shared by *all* instances + # of the class. + # + # This is an implementation of the Flyweight design pattern, which + # allows multiple objects to share some state. + # + # This can quickly become a mess if the shared state is mutable, + # note that here, once a font is loaded it does not change. + # This avoids nearly all pitfalls associated with this approach. + _fonts: dict[str, pygame.font.Font] = {} + + # this method is attached to the class `Text`, not individual instances + # like normal methods (which take self as their implicit parameter) + @classmethod + def make_font(cls, name, size, font=None, bold=False, italic=False): + """ + The way fonts work in most graphics libraries requires choosing a font + size, as well as any variation (bold, italic) at the time of creation. + + It would be nice if we could allow individual Text objects vary these, + but doing so would be much more complex or require significantly more + memory. + """ + if font is None: + font = pygame.font.Font(None, size) + else: + path = pygame.font.match_font(font, bold=bold, italic=italic) + font = pygame.font.Font(path, size) + cls._fonts[name] = font + + @classmethod + def get_font(cls, name=None): + if not name: + # None -> default font + # load on demand + if None not in cls._fonts: + cls._fonts[None] = pygame.font.Font(None, DEFAULT_FONT_SIZE) + return cls._fonts[None] + else: + return cls._fonts[name] + + def __init__(self, parent=None): + """ + Text will be centered at `pos` + """ + super().__init__(parent) + self._text = "" + self._rendered = None + self._font = None + + def __repr__(self): + return f"Text(pos={self.pos_vec}, text={self._text}, parent={self._parent})" + + def draw(self, screen): + text_rect = self._rendered.get_rect(center=(self.x, self.y)) + screen.blit(self._rendered, text_rect) + + def text(self, text: str) -> "Doodle": + """ + A setter for the text + """ + self._text = text + # text needs to be rendered once on change to be performant + # doing this in draw would be much slower since it is called + # much more often than the text changes + if not self._font: + self._font = self.get_font() # default font + self._rendered = self._font.render(self._text, True, self._color) + return self + + def font(self, font: str) -> "Doodle": + # TODO: error checking + self._font = self._fonts[font] + return self