integration of functional elements
Python CI / test (push) Successful in 6m46s

This commit is contained in:
2025-11-08 10:17:01 +01:00
parent ea93681aaf
commit 39622c7dd7
11 changed files with 1082 additions and 25 deletions
+196 -3
View File
@@ -1,13 +1,15 @@
from __future__ import annotations
from typing import List, Tuple, Optional, Union
import numpy as np
from pyWebLayout.concrete import Page, Line, Text
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.concrete.functional import LinkText
from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.abstract import Paragraph, Word, Link
from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table
from pyWebLayout.abstract.functional import Button, Form, FormField
from pyWebLayout.abstract.inline import LinkedWord
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
from pyWebLayout.style import Font, Alignment
@@ -340,6 +342,157 @@ def table_layouter(table: Table, page: Page, style: Optional[TableStyle] = None)
return True
def button_layouter(button: Button, page: Page, font: Optional[Font] = None,
padding: Tuple[int, int, int, int] = (4, 8, 4, 8)) -> Tuple[bool, str]:
"""
Layout a button within a given page and register it for callback binding.
This function creates a ButtonText renderable, positions it on the page,
and registers it in the page's callback registry using the button's html_id
(if available) or an auto-generated id.
Args:
button: The abstract Button object to layout
page: The page to layout the button on
font: Optional font for button text (defaults to page default)
padding: Padding around button text (top, right, bottom, left)
Returns:
Tuple of:
- bool: True if button was successfully laid out, False if page ran out of space
- str: The id used to register the button in the callback registry
"""
# Use provided font or create a default one
if font is None:
font = Font(font_size=14, colour=(255, 255, 255))
# Calculate available space
available_height = page.size[1] - page._current_y_offset - page.border_size
# Create ButtonText renderable
button_text = ButtonText(button, font, page.draw, padding=padding)
# Check if button fits on current page
button_height = button_text.size[1]
if button_height > available_height:
return False, ""
# Position the button
x_offset = page.border_size
y_offset = page._current_y_offset
button_text.set_origin(np.array([x_offset, y_offset]))
# Register in callback registry
html_id = button.html_id
registered_id = page.callbacks.register(button_text, html_id=html_id)
# Add to page
page.add_child(button_text)
return True, registered_id
def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = None,
field_height: int = 24) -> Tuple[bool, str]:
"""
Layout a form field within a given page and register it for callback binding.
This function creates a FormFieldText renderable, positions it on the page,
and registers it in the page's callback registry.
Args:
field: The abstract FormField object to layout
page: The page to layout the field on
font: Optional font for field label (defaults to page default)
field_height: Height of the input field area
Returns:
Tuple of:
- bool: True if field was successfully laid out, False if page ran out of space
- str: The id used to register the field in the callback registry
"""
# Use provided font or create a default one
if font is None:
font = Font(font_size=12, colour=(0, 0, 0))
# Calculate available space
available_height = page.size[1] - page._current_y_offset - page.border_size
# Create FormFieldText renderable
field_text = FormFieldText(field, font, page.draw, field_height=field_height)
# Check if field fits on current page
total_field_height = field_text.size[1]
if total_field_height > available_height:
return False, ""
# Position the field
x_offset = page.border_size
y_offset = page._current_y_offset
field_text.set_origin(np.array([x_offset, y_offset]))
# Register in callback registry (use field name as html_id fallback)
html_id = getattr(field, '_html_id', None) or field.name
registered_id = page.callbacks.register(field_text, html_id=html_id)
# Add to page
page.add_child(field_text)
return True, registered_id
def form_layouter(form: Form, page: Page, font: Optional[Font] = None,
field_spacing: int = 10) -> Tuple[bool, List[str]]:
"""
Layout a complete form with all its fields within a given page.
This function creates FormFieldText renderables for all fields in the form,
positions them vertically, and registers both the form and its fields in
the page's callback registry.
Args:
form: The abstract Form object to layout
page: The page to layout the form on
font: Optional font for field labels (defaults to page default)
field_spacing: Vertical spacing between fields in pixels
Returns:
Tuple of:
- bool: True if form was successfully laid out, False if page ran out of space
- List[str]: List of registered ids for all fields (empty if layout failed)
"""
# Use provided font or create a default one
if font is None:
font = Font(font_size=12, colour=(0, 0, 0))
# Track registered field ids
field_ids = []
# Layout each field in the form
for field_name, field in form._fields.items():
# Add spacing before each field (except the first)
if field_ids:
page._current_y_offset += field_spacing
# Layout the field
success, field_id = form_field_layouter(field, page, font)
if not success:
# Couldn't fit this field, return failure
return False, []
field_ids.append(field_id)
# Register the form itself (optional, for form submission)
# Note: The form doesn't have a visual representation, but we can track it
# for submission callbacks
# form_id = page.callbacks.register(form, html_id=form.html_id)
return True, field_ids
class DocumentLayouter:
"""
Document layouter that orchestrates layout of various abstract elements.
@@ -415,14 +568,46 @@ class DocumentLayouter:
"""
return table_layouter(table, self.page, style)
def layout_document(self, elements: List[Union[Paragraph, AbstractImage, Table]]) -> bool:
def layout_button(self, button: Button, font: Optional[Font] = None,
padding: Tuple[int, int, int, int] = (4, 8, 4, 8)) -> Tuple[bool, str]:
"""
Layout a list of abstract elements (paragraphs, images, and tables).
Layout a button using the button_layouter.
Args:
button: The abstract Button object to layout
font: Optional font for button text
padding: Padding around button text
Returns:
Tuple of (success, registered_id)
"""
return button_layouter(button, self.page, font, padding)
def layout_form(self, form: Form, font: Optional[Font] = None,
field_spacing: int = 10) -> Tuple[bool, List[str]]:
"""
Layout a form using the form_layouter.
Args:
form: The abstract Form object to layout
font: Optional font for field labels
field_spacing: Vertical spacing between fields
Returns:
Tuple of (success, list_of_field_ids)
"""
return form_layouter(form, self.page, font, field_spacing)
def layout_document(self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool:
"""
Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms).
This method delegates to specialized layouters based on element type:
- Paragraphs are handled by layout_paragraph
- Images are handled by layout_image
- Tables are handled by layout_table
- Buttons are handled by layout_button
- Forms are handled by layout_form
Args:
elements: List of abstract elements to layout
@@ -443,5 +628,13 @@ class DocumentLayouter:
success = self.layout_table(element)
if not success:
return False
elif isinstance(element, Button):
success, _ = self.layout_button(element)
if not success:
return False
elif isinstance(element, Form):
success, _ = self.layout_form(element)
if not success:
return False
# Future: elif isinstance(element, CodeBlock): use code_layouter
return True