313 lines
9.8 KiB
Python
313 lines
9.8 KiB
Python
#!/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()
|