Tables now use ""dynamic page" allowing the contents to be anything that can be rendered ion a page.
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user