#!/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()