diff --git a/README.md b/README.md index 5f74296..c8167c8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ PyWebLayout is a Python library for HTML-like layout and rendering to paginated - ↔️ **Text Alignment** - Left, center, right, and justified text - 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more - 📊 **Table Rendering** - Full HTML table support with headers, borders, and styling +- 🔘 **Interactive Elements** - Buttons, forms, and links with callback support ### Architecture - **Abstract/Concrete Separation** - Clean separation between content structure and rendering @@ -111,6 +112,13 @@ The library supports various page layouts and configurations: HTML tables with headers and styling + + + Interactive Elements
+ Interactive Elements
+ Buttons, forms, and callback binding + + ## Examples @@ -123,6 +131,7 @@ The `examples/` directory contains working demonstrations: - **[03_page_layouts.py](examples/03_page_layouts.py)** - Different page configurations - **[04_table_rendering.py](examples/04_table_rendering.py)** - HTML table rendering with styling - **[05_table_with_images.py](examples/05_table_with_images.py)** - Tables with embedded images +- **[06_functional_elements_demo.py](examples/06_functional_elements_demo.py)** - Interactive buttons and forms with callbacks ### Advanced Examples - **[html_multipage_simple.py](examples/html_multipage_simple.py)** - Multi-page HTML rendering diff --git a/docs/images/example_06_functional_elements.png b/docs/images/example_06_functional_elements.png new file mode 100644 index 0000000..2ff8f1b Binary files /dev/null and b/docs/images/example_06_functional_elements.png differ diff --git a/examples/06_functional_elements_demo.py b/examples/06_functional_elements_demo.py new file mode 100644 index 0000000..7ae5722 --- /dev/null +++ b/examples/06_functional_elements_demo.py @@ -0,0 +1,292 @@ +""" +Demonstration of functional elements (buttons, forms, links) with callback binding. + +This example shows how to: +1. Create functional elements programmatically +2. Layout them on a page +3. Bind callbacks after layout using the CallbackRegistry +4. Simulate user interactions + +This pattern is useful for: +- Manual GUI construction +- Applications where callbacks need access to runtime state +- Interactive document interfaces +""" + +from pyWebLayout.concrete import Page +from pyWebLayout.abstract.functional import Button, Form, FormField, FormFieldType +from pyWebLayout.abstract import Paragraph, Word +from pyWebLayout.style import Font +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.layout.document_layouter import DocumentLayouter +import numpy as np + + +class SimpleApp: + """ + A simple application that demonstrates functional element usage. + + This app has: + - A settings form + - Save and Cancel buttons + - Application state that callbacks can access + """ + + def __init__(self): + self.settings = { + "username": "", + "theme": "light", + "notifications": True + } + self.saved = False + + def on_save_click(self, point, **kwargs): + """Callback for save button""" + print(f"Save button clicked at {point}") + self.saved = True + print("Settings saved!") + return "saved" + + def on_cancel_click(self, point, **kwargs): + """Callback for cancel button""" + print(f"Cancel button clicked at {point}") + print("Changes cancelled!") + return "cancelled" + + def on_reset_click(self, point, **kwargs): + """Callback for reset button""" + print(f"Reset button clicked at {point}") + self.settings = { + "username": "", + "theme": "light", + "notifications": True + } + print("Settings reset to defaults!") + return "reset" + + +def create_settings_page(): + """ + Create a settings page with functional elements. + + Returns: + Tuple of (page, app, element_ids) where element_ids maps + semantic names to registered callback ids + """ + # Create the application instance + app = SimpleApp() + + # Create page + page = Page(size=(600, 800), style=PageStyle(border_width=10)) + layouter = DocumentLayouter(page) + + # Create content + font = Font(font_size=16, colour=(0, 0, 0)) + + # Title paragraph + title_font = Font(font_size=24, colour=(0, 0, 100)) + title = Paragraph(title_font) + title.add_word(Word("Settings", title_font)) + + # Description paragraph + desc = Paragraph(font) + desc.add_word(Word("Configure", font)) + desc.add_word(Word("your", font)) + desc.add_word(Word("application", font)) + desc.add_word(Word("preferences", font)) + desc.add_word(Word("below.", font)) + + # Layout title and description + layouter.layout_paragraph(title) + page._current_y_offset += 10 # Add some spacing + layouter.layout_paragraph(desc) + page._current_y_offset += 20 # Add more spacing before form + + # Create form + settings_form = Form( + form_id="settings-form", + action="/save-settings", + html_id="settings-form" + ) + + # Add form fields + username_field = FormField( + name="username", + field_type=FormFieldType.TEXT, + label="Username", + value="john_doe" + ) + + theme_field = FormField( + name="theme", + field_type=FormFieldType.SELECT, + label="Theme", + value="light", + options=[("light", "Light"), ("dark", "Dark")] + ) + + notifications_field = FormField( + name="notifications", + field_type=FormFieldType.CHECKBOX, + label="Enable Notifications", + value=True + ) + + settings_form.add_field(username_field) + settings_form.add_field(theme_field) + settings_form.add_field(notifications_field) + + # Layout the form + success, field_ids = layouter.layout_form(settings_form) + + if not success: + print("Warning: Form didn't fit on page!") + + page._current_y_offset += 20 # Spacing before buttons + + # Create buttons (NO callbacks yet - will be bound later) + save_button = Button( + label="Save Settings", + callback=None, # No callback yet! + html_id="save-btn" + ) + + cancel_button = Button( + label="Cancel", + callback=None, # No callback yet! + html_id="cancel-btn" + ) + + reset_button = Button( + label="Reset to Defaults", + callback=None, # No callback yet! + html_id="reset-btn" + ) + + # Layout buttons + button_font = Font(font_size=14, colour=(255, 255, 255)) + success1, save_id = layouter.layout_button(save_button, font=button_font) + page._current_y_offset += 10 # Spacing between buttons + success2, cancel_id = layouter.layout_button(cancel_button, font=button_font) + page._current_y_offset += 10 + success3, reset_id = layouter.layout_button(reset_button, font=button_font) + + # ============================================================== + # IMPORTANT: Callbacks are bound AFTER layout is complete + # This allows callbacks to access the application instance + # ============================================================== + + # Bind callbacks using the page's callback registry + page.callbacks.set_callback("save-btn", app.on_save_click) + page.callbacks.set_callback("cancel-btn", app.on_cancel_click) + page.callbacks.set_callback("reset-btn", app.on_reset_click) + + # Track element ids for later reference + element_ids = { + "save_button": save_id, + "cancel_button": cancel_id, + "reset_button": reset_id, + "form_fields": field_ids + } + + return page, app, element_ids + + +def demonstrate_callback_binding(): + """Demonstrate various callback binding patterns""" + + print("=" * 60) + print("Functional Elements Demo: Manual GUI Construction") + print("=" * 60) + print() + + # Create the page + page, app, element_ids = create_settings_page() + + print(f"Page created with {page.callbacks.count()} interactable elements") + print() + + # Show what's registered + print("Registered interactables:") + for id_name in page.callbacks.get_all_ids(): + print(f" - {id_name}") + print() + + # Show breakdown by type + print("Breakdown by type:") + for type_name in page.callbacks.get_all_types(): + count = page.callbacks.count_by_type(type_name) + print(f" - {type_name}: {count}") + print() + + # Simulate user clicking the save button + print("Simulating user interaction:") + print("-" * 60) + print() + + # Get the save button + save_button = page.callbacks.get_by_id("save-btn") + print(f"Retrieved save button: {save_button}") + + # Simulate a click at position (50, 200) + click_point = np.array([50, 200]) + print(f"Simulating click at {click_point}...") + result = save_button.interact(click_point) + print(f"Button interaction returned: {result}") + print(f"App.saved state: {app.saved}") + print() + + # Simulate clicking cancel + cancel_button = page.callbacks.get_by_id("cancel-btn") + print("Simulating cancel button click...") + result = cancel_button.interact(click_point) + print(f"Button interaction returned: {result}") + print() + + # Simulate clicking reset + reset_button = page.callbacks.get_by_id("reset-btn") + print("Simulating reset button click...") + result = reset_button.interact(click_point) + print(f"Button interaction returned: {result}") + print() + + # Demonstrate batch callback modification + print("-" * 60) + print("Demonstrating batch callback modification:") + print() + + def log_all_clicks(point, **kwargs): + """Generic click logger""" + print(f" [LOG] Button clicked at {point}") + return "logged" + + # Set this callback for all buttons + count = page.callbacks.set_callbacks_by_type("button", log_all_clicks) + print(f"Set logging callback for {count} buttons") + print() + + # Now clicking any button will just log + print("Clicking save button again (now with logging callback):") + result = save_button.interact(click_point) + print(f"Returned: {result}") + print() + + # Render the page + print("-" * 60) + print("Rendering page...") + image = page.render() + print(f"Page rendered: {image.size}") + + # Save to file + output_path = "functional_elements_demo.png" + image.save(output_path) + print(f"Saved to: {output_path}") + print() + + print("=" * 60) + print("Demo complete!") + print("=" * 60) + + +if __name__ == "__main__": + demonstrate_callback_binding() diff --git a/examples/README.md b/examples/README.md index 9384445..6f3bf95 100644 --- a/examples/README.md +++ b/examples/README.md @@ -83,6 +83,24 @@ Demonstrates: ![Table with Images Example](../docs/images/example_05_table_with_images.png) +### 06. Functional Elements (Interactive) +**`06_functional_elements_demo.py`** - Interactive buttons and forms with callbacks + +```bash +python 06_functional_elements_demo.py +``` + +Demonstrates: +- Creating interactive buttons +- Building forms with multiple field types +- Post-layout callback binding +- CallbackRegistry system for managing interactables +- Accessing application state from callbacks +- Batch callback operations +- Simulating user interactions + +![Functional Elements Example](../docs/images/example_06_functional_elements.png) + ## Advanced Examples ### HTML Rendering @@ -106,6 +124,7 @@ python 02_text_and_layout.py python 03_page_layouts.py python 04_table_rendering.py python 05_table_with_images.py +python 06_functional_elements_demo.py ``` Output images are saved to the `docs/images/` directory. diff --git a/highlights/test_highlights.json b/highlights/test_highlights.json deleted file mode 100644 index 3fae1cd..0000000 --- a/highlights/test_highlights.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "document_id": "test", - "highlights": [] -} \ No newline at end of file diff --git a/pyWebLayout/abstract/functional.py b/pyWebLayout/abstract/functional.py index 679c14d..84d3994 100644 --- a/pyWebLayout/abstract/functional.py +++ b/pyWebLayout/abstract/functional.py @@ -19,27 +19,30 @@ class Link(Interactable): or to trigger API calls for functionality like settings management. """ - def __init__(self, - location: str, + def __init__(self, + location: str, link_type: LinkType = LinkType.INTERNAL, callback: Optional[Callable] = None, params: Optional[Dict[str, Any]] = None, - title: Optional[str] = None): + title: Optional[str] = None, + html_id: Optional[str] = None): """ Initialize a link. - + Args: location: The target location or identifier for this link link_type: The type of link (internal, external, API, function) callback: Optional callback function to execute when the link is activated params: Optional parameters to pass to the callback or API title: Optional title/tooltip for the link + html_id: Optional HTML id attribute (from ) for callback binding """ super().__init__(callback) self._location = location self._link_type = link_type self._params = params or {} self._title = title + self._html_id = html_id @property def location(self) -> str: @@ -60,7 +63,12 @@ class Link(Interactable): def title(self) -> Optional[str]: """Get the title/tooltip for this link""" return self._title - + + @property + def html_id(self) -> Optional[str]: + """Get the HTML id attribute for callback binding""" + return self._html_id + def execute(self, point=None) -> Any: """ Execute the link action based on its type. @@ -88,24 +96,27 @@ class Button(Interactable): Buttons are similar to function links but are rendered differently. """ - def __init__(self, + def __init__(self, label: str, callback: Callable, params: Optional[Dict[str, Any]] = None, - enabled: bool = True): + enabled: bool = True, + html_id: Optional[str] = None): """ Initialize a button. - + Args: label: The text label for the button callback: The function to execute when the button is clicked params: Optional parameters to pass to the callback enabled: Whether the button is initially enabled + html_id: Optional HTML id attribute (from