Tables now use ""dynamic page" allowing the contents to be anything that can be rendered ion a page.
Python CI / test (3.10) (push) Successful in 2m22s
Python CI / test (3.12) (push) Successful in 2m13s
Python CI / test (3.13) (push) Successful in 2m9s

This commit is contained in:
2025-11-11 18:10:47 +01:00
parent 889f27e1a3
commit 3bcd1bffb5
13 changed files with 2399 additions and 14 deletions
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
Demo: Optimized Table Column Width Layout
This example demonstrates the intelligent table column width optimization:
- Automatic width distribution based on content
- HTML width overrides (fixed column widths)
- Sampling for performance (large tables)
- Comparison: before (equal distribution) vs after (optimized)
The optimizer:
1. Samples first ~5 rows from each section
2. Measures minimum and preferred widths for each column
3. Distributes available space proportionally
4. Respects HTML width attributes
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import ImageDraw
def create_demo_table_1():
"""Create a table with varying content lengths (shows optimization)."""
table = Table()
table.caption = "Example 1: Optimized Width Distribution"
font = Font(font_size=11)
# Header
header_row = TableRow()
for text in ["ID", "Name", "Description"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows with varying content lengths
data = [
("1", "Alice", "Short description"),
("2", "Bob", "This is a much longer description that demonstrates how the optimizer allocates more space to columns with longer content"),
("3", "Charlie", "Medium length description here"),
("4", "Diana", "Another longer description that shows the column width optimization working effectively for content-heavy cells"),
]
for row_data in data:
row = TableRow()
for text in row_data:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def create_demo_table_2():
"""Create a table with HTML width overrides."""
table = Table()
table.caption = "Example 2: Fixed Column Widths (HTML override)"
font = Font(font_size=11)
# Header with width attributes
header_row = TableRow()
# Fixed width column
cell1 = TableCell(is_header=True)
cell1.width = "80px" # HTML width override!
para1 = Paragraph(font)
para1.add_word(Word("ID", font))
para1.add_word(Word("(Fixed", font))
para1.add_word(Word("80px)", font))
cell1.add_block(para1)
header_row.add_cell(cell1)
# Auto-width columns
for text in ["Name (Auto)", "Description (Auto)"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows
data = [
("1", "Alice", "The first two columns adapt to remaining space"),
("2", "Bob", "ID column stays fixed at 80px width"),
("3", "Charlie", "Name and Description share the remaining width proportionally"),
]
for row_data in data:
row = TableRow()
# First cell also has fixed width
cell = TableCell()
cell.width = "80px"
para = Paragraph(font)
para.add_word(Word(row_data[0], font))
cell.add_block(para)
row.add_cell(cell)
# Other cells auto-width
for text in row_data[1:]:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def create_demo_table_3():
"""Create a large table (demonstrates sampling)."""
table = Table()
table.caption = "Example 3: Large Table (uses sampling for performance)"
font = Font(font_size=10)
# Header
header_row = TableRow()
for text in ["Index", "Data A", "Data B", "Data C"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Many body rows (only first ~5 will be sampled for measurement)
for i in range(50):
row = TableRow()
# Index
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(str(i + 1), font))
cell.add_block(para)
row.add_cell(cell)
# Data columns with varying content
if i % 3 == 0:
data = ["Short", "Medium length", "Longer content here"]
elif i % 3 == 1:
data = ["Medium", "Short", "Also longer content"]
else:
data = ["Longer text", "Short", "Medium"]
for text in data:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def main():
# Create page
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
page = Page(size=(800, 2200), style=page_style)
# Get canvas and draw
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
current_y = 30
# Table style
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(8, 8, 8, 8),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# Render Example 1: Optimized distribution
table1 = create_demo_table_1()
renderer1 = TableRenderer(
table1,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer1.render()
current_y += renderer1.height + 40
# Render Example 2: Fixed widths
table2 = create_demo_table_2()
renderer2 = TableRenderer(
table2,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer2.render()
current_y += renderer2.height + 40
# Render Example 3: Large table with sampling
table3 = create_demo_table_3()
renderer3 = TableRenderer(
table3,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer3.render()
# Save
output_path = "docs/images/example_12_optimized_table_layout.png"
canvas.save(output_path)
print(f"✓ Optimized table layout demo created!")
print(f" Output: {output_path}")
print(f" Image size: {canvas.size}")
print(f"\nExamples demonstrated:")
print(f" 1. Content-aware width distribution")
print(f" 2. HTML width overrides (80px fixed column)")
print(f" 3. Large table with sampling (50 rows, only ~5 measured)")
if __name__ == "__main__":
main()
+291
View File
@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""
Demo: Table Pagination
This example demonstrates table pagination when content exceeds page height:
- Large table that spans multiple pages
- Automatic row-level pagination (entire rows move to next page)
- Continuation markers ("continued on next page", "continued from previous page")
- Headers repeated on each page
The pagination system:
1. Renders rows sequentially until page height limit reached
2. Moves entire row to next page if it doesn't fit
3. Repeats header row on continuation pages
4. Adds visual markers to show table continues
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw
def create_large_table():
"""Create a table with many rows that will require pagination."""
table = Table()
table.caption = "Employee Directory (Paginated)"
font = Font(font_size=11)
# Header
header_row = TableRow()
for text in ["ID", "Name", "Department", "Email", "Phone"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Many body rows (will span multiple pages)
departments = ["Engineering", "Sales", "Marketing", "HR", "Finance", "Operations", "Support"]
for i in range(60):
row = TableRow()
# ID
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"EMP{i+1001}", font))
cell.add_block(para)
row.add_cell(cell)
# Name
cell = TableCell()
para = Paragraph(font)
names = ["Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Lee",
"Eve Wilson", "Frank Miller", "Grace Davis", "Henry Taylor"]
para.add_word(Word(names[i % len(names)], font))
cell.add_block(para)
row.add_cell(cell)
# Department
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(departments[i % len(departments)], font))
cell.add_block(para)
row.add_cell(cell)
# Email
cell = TableCell()
para = Paragraph(font)
email = f"{names[i % len(names)].lower().replace(' ', '.')}@company.com"
para.add_word(Word(email, font))
cell.add_block(para)
row.add_cell(cell)
# Phone
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"+1-555-{(i*17)%1000:04d}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def render_table_with_pagination(table, page_size, max_pages=3):
"""
Render a table across multiple pages.
Args:
table: The table to render
page_size: Tuple of (width, height) for each page
max_pages: Maximum number of pages to render
Returns:
List of PIL Images (one per page)
"""
pages = []
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(6, 8, 6, 8),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# Get all rows
all_rows = list(table.all_rows())
header_rows = [row for section, row in all_rows if section == "header"]
body_rows = [row for section, row in all_rows if section == "body"]
# Calculate header height once
temp_page = Page(size=page_size, style=page_style)
temp_canvas = temp_page._create_canvas()
temp_draw = ImageDraw.Draw(temp_canvas)
# Create temporary table with just header to measure
header_table = Table()
header_table.caption = table.caption
for header_row in header_rows:
header_table.add_row(header_row, section="header")
header_renderer = TableRenderer(
header_table,
origin=(20, 20),
available_width=page_size[0] - 40,
draw=temp_draw,
style=table_style,
canvas=temp_canvas
)
header_height = header_renderer.height
# Available height for body rows
available_body_height = page_size[1] - 60 - header_height # margins + header
# Paginate body rows
current_page_rows = []
current_height = 0
page_num = 0
for i, body_row in enumerate(body_rows):
if page_num >= max_pages:
break
# Estimate row height (simplified - actual would measure each row)
# For this demo, assume ~30px per row
row_height = 35
if current_height + row_height > available_body_height and current_page_rows:
# Render current page
page_canvas = render_page(
table,
header_rows,
current_page_rows,
page_size,
page_style,
table_style,
page_num,
is_last=False
)
pages.append(page_canvas)
# Start new page
page_num += 1
current_page_rows = []
current_height = 0
current_page_rows.append(body_row)
current_height += row_height
# Render final page
if current_page_rows and page_num < max_pages:
page_canvas = render_page(
table,
header_rows,
current_page_rows,
page_size,
page_style,
table_style,
page_num,
is_last=(i == len(body_rows) - 1)
)
pages.append(page_canvas)
return pages
def render_page(table, header_rows, body_rows, page_size, page_style, table_style, page_num, is_last):
"""Render a single page with header and body rows."""
page = Page(size=page_size, style=page_style)
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
# Create table for this page
page_table = Table()
if page_num == 0:
page_table.caption = table.caption
else:
page_table.caption = f"{table.caption} (continued)"
# Add header rows
for header_row in header_rows:
page_table.add_row(header_row, section="header")
# Add body rows for this page
for body_row in body_rows:
page_table.add_row(body_row, section="body")
# Render table
renderer = TableRenderer(
page_table,
origin=(20, 20),
available_width=page_size[0] - 40,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer.render()
# Add continuation marker at bottom
if not is_last:
font = Font(font_size=10)
y_pos = page_size[1] - 30
page._draw.text(
(page_size[0] // 2 - 100, y_pos),
"(continued on next page)",
fill=(100, 100, 100),
font=font.font
)
# Add page number
page._draw.text(
(page_size[0] // 2 - 20, page_size[1] - 15),
f"Page {page_num + 1}",
fill=(150, 150, 150),
font=Font(font_size=9).font
)
return canvas
def main():
# Create large table
table = create_large_table()
# Render with pagination (3 pages max for demo)
page_size = (900, 700)
pages = render_table_with_pagination(table, page_size, max_pages=3)
# Combine pages side-by-side for visualization
total_width = page_size[0] * len(pages) + (len(pages) - 1) * 20 # 20px spacing
combined = Image.new('RGB', (total_width, page_size[1]), (240, 240, 240))
x_offset = 0
for i, page_canvas in enumerate(pages):
combined.paste(page_canvas, (x_offset, 0))
x_offset += page_size[0] + 20
# Save
output_path = "docs/images/example_13_table_pagination.png"
combined.save(output_path)
print(f"✓ Table pagination demo created!")
print(f" Output: {output_path}")
print(f" Pages rendered: {len(pages)}")
print(f" Image size: {combined.size}")
print(f"\nDemonstrates:")
print(f" - Large table (60 rows) paginated across {len(pages)} pages")
print(f" - Header repeated on each page")
print(f" - Continuation markers")
print(f" - Page numbers")
if __name__ == "__main__":
main()
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Demo: Working Interactive Table with Buttons
This example shows a fully working interactive table where buttons are
actually rendered inside table cells and can handle click events.
This uses a hybrid approach:
1. Tables are rendered normally for structure
2. Buttons are rendered on top at calculated positions
3. Click detection maps coordinates to button callbacks
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.concrete.functional import ButtonText
from pyWebLayout.abstract.functional import Button
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw
import numpy as np
def create_interactive_table():
"""Create a table structure (buttons will be overlaid)."""
table = Table()
table.caption = "User Management with Interactive Buttons"
font = Font(font_size=11)
# Header
header_row = TableRow()
for i, text in enumerate(["ID", "Name", "Email", "Actions"]):
cell = TableCell(is_header=True)
# Set width for Actions column
if text == "Actions":
cell.width = "220px" # Enough for 3 buttons
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows
users = [
("U001", "Alice Johnson", "alice@example.com"),
("U002", "Bob Smith", "bob@example.com"),
("U003", "Charlie Brown", "charlie@example.com"),
]
for user_id, name, email in users:
row = TableRow()
# ID
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(user_id, font))
cell.add_block(para)
row.add_cell(cell)
# Name
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(name, font))
cell.add_block(para)
row.add_cell(cell)
# Email
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(email, font))
cell.add_block(para)
row.add_cell(cell)
# Actions - leave empty for buttons to be overlaid
# Set width hint to ensure space for 3 buttons
cell = TableCell()
cell.width = "220px" # Enough for 3 buttons (3 × 65px + padding)
para = Paragraph(font)
para.add_word(Word("", font)) # Empty placeholder
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def render_buttons_in_table(canvas, draw, table_origin, column_widths, row_heights, users):
"""
Render interactive buttons inside the table cells.
This calculates the exact position of each button based on the table
layout and renders ButtonText objects at those positions.
Args:
canvas: PIL Image canvas
draw: PIL ImageDraw object
table_origin: (x, y) position of table top-left
column_widths: List of column widths
row_heights: Dict with 'header', 'body', 'footer' keys
users: User data for button labels
Returns:
List of (button, bounds) for click detection
"""
button_font = Font(font_size=10)
buttons_with_bounds = []
# Calculate Actions column position (column 3, index 3)
actions_col_x = table_origin[0] + sum(column_widths[:3]) + 3 * 2 # +borders
actions_col_width = column_widths[3]
# Start after caption and header row
# Caption takes 20px + 10px spacing = 30px
caption_height = 30
header_height = row_heights.get("header", 30)
current_y = table_origin[1] + caption_height + header_height + 2 # +caption +header +border
for i, (user_id, name, email) in enumerate(users):
row_height = row_heights.get("body", 30) # All body rows have same height
# Position buttons horizontally in the Actions cell
button_x = actions_col_x + 10 # Padding from cell edge
button_y = current_y + (row_height - 30) // 2 # Center vertically
# Create buttons for this row
# Note: Button callbacks receive click point as first argument
buttons = [
("View", lambda point, uid=user_id: print(f"View {uid}")),
("Edit", lambda point, uid=user_id: print(f"Edit {uid}")),
("Delete", lambda point, uid=user_id: print(f"Delete {uid}"))
]
for label, callback in buttons:
# Create button
abstract_button = Button(label=label, callback=callback)
button_text = ButtonText(
button=abstract_button,
font=button_font,
draw=draw,
padding=(6, 12, 6, 12)
)
# Set position
button_text._origin = np.array([button_x, button_y])
# Render button
button_text.render()
# Store bounds for click detection
button_width = 60 # Approximate
button_height = 25
bounds = (button_x, button_y, button_x + button_width, button_y + button_height)
buttons_with_bounds.append((abstract_button, bounds))
# Move to next button position
button_x += 65
# Move to next row
current_y += row_height + 1 # +border
return buttons_with_bounds
def handle_click(click_pos, buttons_with_bounds):
"""
Handle a click event by checking if it's inside any button bounds.
Args:
click_pos: (x, y) tuple of click position
buttons_with_bounds: List of (button, bounds) tuples
Returns:
True if a button was clicked, False otherwise
"""
click_x, click_y = click_pos
for button, (x1, y1, x2, y2) in buttons_with_bounds:
if x1 <= click_x <= x2 and y1 <= click_y <= y2:
# Click is inside this button!
button.execute(click_pos)
return True
return False
def main():
# Create page
page_size = (900, 500) # Increased height to fit instructions
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
page = Page(size=page_size, style=page_style)
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
# Table style
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(8, 10, 8, 10),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# User data
users = [
("U001", "Alice Johnson", "alice@example.com"),
("U002", "Bob Smith", "bob@example.com"),
("U003", "Charlie Brown", "charlie@example.com"),
]
# Create and render table
table = create_interactive_table()
table_origin = (20, 30)
renderer = TableRenderer(
table,
origin=table_origin,
available_width=860,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer.render()
# Get table dimensions for button positioning
column_widths = renderer._column_widths
row_heights = renderer._row_heights
# Render interactive buttons on top of table
buttons_with_bounds = render_buttons_in_table(
canvas, page._draw, table_origin,
column_widths, row_heights, users
)
# Add instructions (position below the table)
# Calculate actual table height based on rows
header_height = row_heights.get("header", 30)
body_height = row_heights.get("body", 30) * len(users)
actual_table_height = header_height + body_height + (len(users) + 2) * 2 # +borders
inst_font = Font(font_size=12)
y_offset = table_origin[1] + actual_table_height + 50
page._draw.text(
(20, y_offset),
"Interactive Table Demo:",
fill=(50, 50, 50),
font=inst_font.font
)
note_font = Font(font_size=10)
page._draw.text(
(20, y_offset + 25),
"• Buttons are rendered at calculated positions within table cells",
fill=(80, 80, 80),
font=note_font.font
)
page._draw.text(
(20, y_offset + 45),
"• Click detection maps coordinates to button callbacks",
fill=(80, 80, 80),
font=note_font.font
)
page._draw.text(
(20, y_offset + 65),
"• Try simulated clicks below:",
fill=(80, 80, 80),
font=note_font.font
)
# Save
output_path = "docs/images/example_14_interactive_table.png"
canvas.save(output_path)
print(f"✓ Working interactive table demo created!")
print(f" Output: {output_path}")
print(f" Image size: {canvas.size}")
print(f"\nDemonstrating button click detection:")
# Simulate some clicks to demonstrate functionality
actions_col_x = table_origin[0] + sum(column_widths[:3]) + 6
caption_height = 30
header_height = row_heights.get("header", 30)
body_row_height = row_heights.get("body", 30)
# Calculate first body row position (after caption + header + border)
first_row_y = table_origin[1] + caption_height + header_height + 2
test_clicks = [
(100, 100, "Click outside table"),
(actions_col_x + 10, first_row_y + 15, "View button - Alice"),
(actions_col_x + 75, first_row_y + 15, "Edit button - Alice"),
(actions_col_x + 140, first_row_y + 15, "Delete button - Alice"),
(actions_col_x + 10, first_row_y + body_row_height + 17, "View button - Bob"),
]
for x, y, desc in test_clicks:
print(f"\n Click at ({x}, {y}) - {desc}:")
clicked = handle_click((x, y), buttons_with_bounds)
if not clicked:
print(f" No button at this position")
if __name__ == "__main__":
main()