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
+
+ 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:

+### 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
+
+
+
## 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