443 lines
13 KiB
Python
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
|