443 lines
13 KiB
Python

from abc import ABC
from typing import Optional, Tuple, TYPE_CHECKING, Any, Dict
import numpy as np
if TYPE_CHECKING:
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
class Renderable(ABC):
"""
Abstract base class for any object that can be rendered to an image.
All renderable objects must implement the render method.
"""
def render(self):
"""
Render the object to an image.
Returns:
PIL.Image: The rendered image
"""
@property
def origin(self):
return self._origin
class Interactable(ABC):
"""
Abstract base class for any object that can be interacted with.
Interactable objects must have a callback that is executed when interacted with.
"""
def __init__(self, callback=None):
"""
Initialize an interactable object.
Args:
callback: The function to call when this object is interacted with
"""
self._callback = callback
def interact(self, point: np.generic):
"""
Handle interaction at the given point.
Args:
point: The coordinates of the interaction
Returns:
The result of calling the callback function with the point
"""
if self._callback is None:
return None
return self._callback(point)
class Layoutable(ABC):
"""
Abstract base class for any object that can be laid out.
Layoutable objects must implement the layout method which arranges their contents.
"""
def layout(self):
"""
Layout the object's contents.
This method should be called before rendering to properly arrange the object's contents.
"""
class Queriable(ABC):
def in_object(self, point: np.generic):
"""
check if a point is in the object
"""
point_array = np.array(point)
relative_point = point_array - self._origin
return np.all((0 <= relative_point) & (relative_point < self.size))
# ==============================================================================
# Mixins - Reusable components for common patterns
# ==============================================================================
class Hierarchical:
"""
Mixin providing parent-child relationship management.
Classes using this mixin can track their parent in a document hierarchy.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._parent: Optional[Any] = None
@property
def parent(self) -> Optional[Any]:
"""Get the parent object containing this object, if any"""
return self._parent
@parent.setter
def parent(self, parent: Any):
"""Set the parent object"""
self._parent = parent
class Geometric:
"""
Mixin providing origin and size properties for positioned elements.
Provides standard geometric properties for elements that have a position
and size in 2D space. Uses numpy arrays for efficient calculations.
"""
def __init__(self, *args, origin=None, size=None, **kwargs):
super().__init__(*args, **kwargs)
self._origin = np.array(origin) if origin is not None else np.array([0, 0])
self._size = np.array(size) if size is not None else np.array([0, 0])
@property
def origin(self) -> np.ndarray:
"""Get the origin (top-left corner) of the element"""
return self._origin
@origin.setter
def origin(self, origin: np.ndarray):
"""Set the origin of the element"""
self._origin = np.array(origin)
@property
def size(self) -> np.ndarray:
"""Get the size (width, height) of the element"""
return self._size
@size.setter
def size(self, size: np.ndarray):
"""Set the size of the element"""
self._size = np.array(size)
def set_origin(self, origin: np.ndarray):
"""Set the origin of this element (alternative setter method)"""
self._origin = np.array(origin)
class Styleable:
"""
Mixin providing style property management.
Classes using this mixin can have a style property that can be
inherited from parents or set explicitly.
"""
def __init__(self, *args, style=None, **kwargs):
super().__init__(*args, **kwargs)
self._style = style
@property
def style(self) -> Optional[Any]:
"""Get the style for this element"""
return self._style
@style.setter
def style(self, style: Any):
"""Set the style for this element"""
self._style = style
class FontRegistry:
"""
Mixin providing font caching and creation with parent delegation.
This mixin allows classes to maintain a local font registry and create/reuse
Font objects efficiently. It supports parent delegation, where font requests
can cascade up to a parent container if one exists.
Classes using this mixin should also use Hierarchical to support parent delegation.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._fonts: Dict[str, 'Font'] = {}
def get_or_create_font(self,
font_path: Optional[str] = None,
font_size: int = 16,
colour: Tuple[int, int, int] = (0, 0, 0),
weight: 'FontWeight' = None,
style: 'FontStyle' = None,
decoration: 'TextDecoration' = None,
background: Optional[Tuple[int, int, int, int]] = None,
language: str = "en_EN",
min_hyphenation_width: Optional[int] = None) -> 'Font':
"""
Get or create a font with the specified properties.
This method will first check if a parent object has a get_or_create_font
method and delegate to it. Otherwise, it will manage fonts locally.
Args:
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
font_size: Size of the font in points.
colour: RGB color tuple for the text.
weight: Font weight (normal or bold).
style: Font style (normal or italic).
decoration: Text decoration (none, underline, or strikethrough).
background: RGBA background color for the text. If None, transparent background.
language: Language code for hyphenation and text processing.
min_hyphenation_width: Minimum width in pixels required for hyphenation.
Returns:
Font object (either existing or newly created)
"""
# Import here to avoid circular imports
from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration
# Set defaults for enum types
if weight is None:
weight = FontWeight.NORMAL
if style is None:
style = FontStyle.NORMAL
if decoration is None:
decoration = TextDecoration.NONE
# If we have a parent with font management, delegate to parent
if hasattr(
self,
'_parent') and self._parent and hasattr(
self._parent,
'get_or_create_font'):
return self._parent.get_or_create_font(
font_path=font_path,
font_size=font_size,
colour=colour,
weight=weight,
style=style,
decoration=decoration,
background=background,
language=language,
min_hyphenation_width=min_hyphenation_width
)
# Otherwise manage our own fonts
# Create a unique key for this font configuration
bg_tuple = background if background else (255, 255, 255, 0)
min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
font_key = (
font_path,
font_size,
colour,
weight.value if hasattr(weight, 'value') else weight,
style.value if hasattr(style, 'value') else style,
decoration.value if hasattr(decoration, 'value') else decoration,
bg_tuple,
language,
min_hyph_width
)
# Convert tuple to string for dictionary key
key_str = str(font_key)
# Check if we already have this font
if key_str in self._fonts:
return self._fonts[key_str]
# Create new font and store it
new_font = Font(
font_path=font_path,
font_size=font_size,
colour=colour,
weight=weight,
style=style,
decoration=decoration,
background=background,
language=language,
min_hyphenation_width=min_hyphenation_width
)
self._fonts[key_str] = new_font
return new_font
class MetadataContainer:
"""
Mixin providing metadata dictionary management.
Allows classes to store and retrieve arbitrary metadata as key-value pairs.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._metadata: Dict[Any, Any] = {}
def set_metadata(self, key: Any, value: Any):
"""
Set a metadata value.
Args:
key: The metadata key
value: The metadata value
"""
self._metadata[key] = value
def get_metadata(self, key: Any) -> Optional[Any]:
"""
Get a metadata value.
Args:
key: The metadata key
Returns:
The metadata value, or None if not set
"""
return self._metadata.get(key)
class BlockContainer:
"""
Mixin providing block management methods.
Provides standard methods for managing block-level children including
adding blocks and creating common block types.
Classes using this mixin should also use Styleable to support style inheritance.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._blocks = []
@property
def blocks(self):
"""Get the list of blocks in this container"""
return self._blocks
def add_block(self, block):
"""
Add a block to this container.
Args:
block: The block to add
"""
self._blocks.append(block)
if hasattr(block, 'parent'):
block.parent = self
def create_paragraph(self, style=None):
"""
Create a new paragraph and add it to this container.
Args:
style: Optional style override. If None, inherits from container
Returns:
The newly created Paragraph object
"""
from pyWebLayout.abstract.block import Paragraph
if style is None and hasattr(self, '_style'):
style = self._style
paragraph = Paragraph(style)
self.add_block(paragraph)
return paragraph
def create_heading(self, level=None, style=None):
"""
Create a new heading and add it to this container.
Args:
level: The heading level (h1-h6)
style: Optional style override. If None, inherits from container
Returns:
The newly created Heading object
"""
from pyWebLayout.abstract.block import Heading, HeadingLevel
if level is None:
level = HeadingLevel.H1
if style is None and hasattr(self, '_style'):
style = self._style
heading = Heading(level, style)
self.add_block(heading)
return heading
class ContainerAware:
"""
Mixin providing support for the create_and_add_to factory pattern.
This is a base that can be extended to provide the create_and_add_to
class method pattern used throughout the abstract module.
Note: This is a framework for future refactoring. Currently, each class
has its own create_and_add_to implementation due to varying constructor
signatures. This mixin provides a foundation for standardizing that pattern.
"""
@classmethod
def _validate_container(cls, container, required_method='add_block'):
"""
Validate that a container has the required method.
Args:
container: The container to validate
required_method: The method name to check for
Raises:
AttributeError: If the container doesn't have the required method
"""
if not hasattr(container, required_method):
raise AttributeError(
f"Container {
type(container).__name__} must have a '{required_method}' method")
@classmethod
def _inherit_style(cls, container, style=None):
"""
Inherit style from container if not explicitly provided.
Args:
container: The container to inherit from
style: Optional explicit style
Returns:
The style to use (explicit or inherited)
"""
if style is not None:
return style
if hasattr(container, 'style'):
return container.style
elif hasattr(container, 'default_style'):
return container.default_style
return None