From 39622c7dd72d5bc90e5b45812f4b2d578c38174a Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 8 Nov 2025 10:17:01 +0100 Subject: [PATCH] integration of functional elements --- README.md | 9 + .../images/example_06_functional_elements.png | Bin 0 -> 18071 bytes examples/06_functional_elements_demo.py | 292 ++++++++++++++++++ examples/README.md | 19 ++ highlights/test_highlights.json | 4 - pyWebLayout/abstract/functional.py | 50 ++- pyWebLayout/concrete/functional.py | 34 +- pyWebLayout/concrete/page.py | 10 + pyWebLayout/core/callback_registry.py | 278 +++++++++++++++++ pyWebLayout/layout/document_layouter.py | 199 +++++++++++- tests/test_callback_registry.py | 212 +++++++++++++ 11 files changed, 1082 insertions(+), 25 deletions(-) create mode 100644 docs/images/example_06_functional_elements.png create mode 100644 examples/06_functional_elements_demo.py delete mode 100644 highlights/test_highlights.json create mode 100644 pyWebLayout/core/callback_registry.py create mode 100644 tests/test_callback_registry.py 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 0000000000000000000000000000000000000000..2ff8f1b3dc65c827b1c39140cc18efbc266aee36 GIT binary patch literal 18071 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 literal 0 HcmV?d00001 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