453 lines
14 KiB
Python
453 lines
14 KiB
Python
"""
|
|
Demonstration of pressed/depressed states for buttons and links with visual feedback.
|
|
|
|
This example shows:
|
|
1. How to use the InteractionHandler for automatic press/release cycles
|
|
2. How to manually manage states for custom event loops
|
|
3. How the dirty flag system tracks when re-rendering is needed
|
|
4. Visual differences between normal, hovered, and pressed states
|
|
|
|
The demo creates a page with buttons and links, then simulates clicking them
|
|
with proper visual feedback timing.
|
|
"""
|
|
|
|
from pyWebLayout.concrete import Page
|
|
from pyWebLayout.concrete.interaction_handler import InteractionHandler, InteractionStateManager
|
|
from pyWebLayout.abstract.functional import Button, Link, LinkType
|
|
from pyWebLayout.abstract import Paragraph, Word
|
|
from pyWebLayout.abstract.inline import LinkedWord
|
|
from pyWebLayout.style import Font
|
|
from pyWebLayout.style.page_style import PageStyle
|
|
from pyWebLayout.layout.document_layouter import DocumentLayouter
|
|
import numpy as np
|
|
import time
|
|
|
|
|
|
def create_interactive_demo_page():
|
|
"""
|
|
Create a page with various interactive elements demonstrating state changes.
|
|
"""
|
|
# Create page
|
|
page = Page(size=(600, 500), style=PageStyle(border_width=10))
|
|
layouter = DocumentLayouter(page)
|
|
|
|
# Create fonts
|
|
title_font = Font(font_size=24, colour=(0, 0, 100))
|
|
body_font = Font(font_size=16, colour=(0, 0, 0))
|
|
button_font = Font(font_size=14, colour=(255, 255, 255))
|
|
|
|
# Title
|
|
title = Paragraph(title_font)
|
|
title.add_word(Word("Interactive", title_font))
|
|
title.add_word(Word("Elements", title_font))
|
|
title.add_word(Word("Demo", title_font))
|
|
layouter.layout_paragraph(title)
|
|
page._current_y_offset += 15
|
|
|
|
# Description
|
|
desc = Paragraph(body_font)
|
|
desc.add_word(Word("Click", body_font))
|
|
desc.add_word(Word("the", body_font))
|
|
desc.add_word(Word("buttons", body_font))
|
|
desc.add_word(Word("and", body_font))
|
|
desc.add_word(Word("links", body_font))
|
|
desc.add_word(Word("below", body_font))
|
|
desc.add_word(Word("to", body_font))
|
|
desc.add_word(Word("see", body_font))
|
|
desc.add_word(Word("pressed", body_font))
|
|
desc.add_word(Word("state", body_font))
|
|
desc.add_word(Word("feedback!", body_font))
|
|
layouter.layout_paragraph(desc)
|
|
page._current_y_offset += 20
|
|
|
|
# Callback functions
|
|
def on_save():
|
|
print("💾 Save button clicked!")
|
|
return "saved"
|
|
|
|
def on_cancel():
|
|
print("❌ Cancel button clicked!")
|
|
return "cancelled"
|
|
|
|
def on_link_click(location, point):
|
|
print(f"🔗 Link clicked: {location} at {point}")
|
|
return location
|
|
|
|
# Create buttons
|
|
save_button = Button(
|
|
label="Save Document",
|
|
callback=lambda point, **kwargs: on_save(),
|
|
html_id="save-btn"
|
|
)
|
|
|
|
cancel_button = Button(
|
|
label="Cancel",
|
|
callback=lambda point, **kwargs: on_cancel(),
|
|
html_id="cancel-btn"
|
|
)
|
|
|
|
# Layout buttons
|
|
success1, save_id = layouter.layout_button(save_button, font=button_font)
|
|
page._current_y_offset += 12
|
|
success2, cancel_id = layouter.layout_button(cancel_button, font=button_font)
|
|
page._current_y_offset += 25
|
|
|
|
# Create paragraph with links
|
|
link_para = Paragraph(body_font)
|
|
link_para.add_word(Word("Visit", body_font))
|
|
link_para.add_word(Word("our", body_font))
|
|
|
|
# Add a link
|
|
internal_link = Link(
|
|
location="https://example.com",
|
|
link_type=LinkType.EXTERNAL,
|
|
callback=on_link_click,
|
|
title="Example website"
|
|
)
|
|
link_para.add_word(LinkedWord(
|
|
"website",
|
|
body_font,
|
|
location="https://example.com",
|
|
link_type=LinkType.EXTERNAL,
|
|
callback=on_link_click,
|
|
title="Example website"
|
|
))
|
|
link_para.add_word(Word("or", body_font))
|
|
|
|
# Add another link
|
|
docs_link = Link(
|
|
location="/docs",
|
|
link_type=LinkType.INTERNAL,
|
|
callback=on_link_click,
|
|
title="Documentation"
|
|
)
|
|
link_para.add_word(LinkedWord(
|
|
"documentation",
|
|
body_font,
|
|
location="/docs",
|
|
link_type=LinkType.INTERNAL,
|
|
callback=on_link_click,
|
|
title="Documentation"
|
|
))
|
|
link_para.add_word(Word("page.", body_font))
|
|
|
|
layouter.layout_paragraph(link_para)
|
|
|
|
return page, save_id, cancel_id
|
|
|
|
|
|
def demo_automatic_interaction():
|
|
"""
|
|
Demonstrate automatic interaction handling with InteractionHandler.
|
|
|
|
This shows the simplest usage pattern where InteractionHandler manages
|
|
the complete press/release cycle automatically.
|
|
"""
|
|
print("=" * 70)
|
|
print("Demo 1: Automatic Interaction with Visual Feedback")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Create the page
|
|
page, save_id, cancel_id = create_interactive_demo_page()
|
|
|
|
# Create interaction handler
|
|
handler = InteractionHandler(page, press_duration_ms=150)
|
|
|
|
print("Initial render:")
|
|
initial_render = page.render()
|
|
initial_render.save("demo_07_initial.png")
|
|
print(f" ✓ Saved: demo_07_initial.png")
|
|
print(f" ✓ Page dirty flag: {page.is_dirty}")
|
|
print()
|
|
|
|
# Get the save button
|
|
save_button = page.callbacks.get_by_id("save-btn")
|
|
click_point = np.array([50, 150])
|
|
|
|
print("Simulating button click with automatic feedback...")
|
|
print(f" → Setting pressed state at t=0ms")
|
|
|
|
# Execute with automatic feedback
|
|
pressed_frame, released_frame, result = handler.execute_with_feedback(
|
|
save_button,
|
|
click_point
|
|
)
|
|
|
|
print(f" → Showing pressed state for 150ms")
|
|
print(f" → Executing callback")
|
|
print(f" → Result: {result}")
|
|
print(f" → Setting released state")
|
|
|
|
# Save the frames
|
|
pressed_frame.save("demo_07_pressed.png")
|
|
print(f" ✓ Saved: demo_07_pressed.png")
|
|
|
|
released_frame.save("demo_07_released.png")
|
|
print(f" ✓ Saved: demo_07_released.png")
|
|
print()
|
|
|
|
|
|
def demo_manual_state_management():
|
|
"""
|
|
Demonstrate manual state management for custom event loops.
|
|
|
|
This shows how an application with its own event loop can manage
|
|
states and check the dirty flag before re-rendering.
|
|
"""
|
|
print("=" * 70)
|
|
print("Demo 2: Manual State Management with Dirty Flag Checking")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Create the page
|
|
page, save_id, cancel_id = create_interactive_demo_page()
|
|
|
|
# Initial render
|
|
print("Initial render:")
|
|
current_frame = page.render()
|
|
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
|
|
print()
|
|
|
|
# Get the cancel button
|
|
cancel_button = page.callbacks.get_by_id("cancel-btn")
|
|
|
|
# Simulate mouse down
|
|
print("Mouse down event:")
|
|
# Set page reference if not already set
|
|
if not hasattr(cancel_button, '_page') or cancel_button._page is None:
|
|
cancel_button.set_page(page)
|
|
cancel_button.set_pressed(True)
|
|
print(f" ✓ Set pressed state")
|
|
print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)")
|
|
|
|
# Check if we need to re-render
|
|
if page.is_dirty:
|
|
print(" → Re-rendering (dirty flag is set)")
|
|
current_frame = page.render()
|
|
current_frame.save("demo_07_manual_pressed.png")
|
|
print(f" ✓ Saved: demo_07_manual_pressed.png")
|
|
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
|
|
print()
|
|
|
|
# Wait a bit
|
|
print("Waiting 150ms for visual feedback...")
|
|
time.sleep(0.15)
|
|
print()
|
|
|
|
# Execute callback
|
|
print("Executing callback:")
|
|
result = cancel_button.interact(np.array([50, 200]))
|
|
print(f" ✓ Result: {result}")
|
|
print()
|
|
|
|
# Simulate mouse up
|
|
print("Mouse up event:")
|
|
cancel_button.set_pressed(False)
|
|
print(f" ✓ Set released state")
|
|
print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)")
|
|
|
|
# Check if we need to re-render
|
|
if page.is_dirty:
|
|
print(" → Re-rendering (dirty flag is set)")
|
|
current_frame = page.render()
|
|
current_frame.save("demo_07_manual_released.png")
|
|
print(f" ✓ Saved: demo_07_manual_released.png")
|
|
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
|
|
print()
|
|
|
|
|
|
def demo_state_manager():
|
|
"""
|
|
Demonstrate the InteractionStateManager for hover/press tracking.
|
|
|
|
This shows how to use the high-level state manager that automatically
|
|
handles hover and press states based on cursor position.
|
|
"""
|
|
print("=" * 70)
|
|
print("Demo 3: InteractionStateManager for Hover and Press Tracking")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Create the page
|
|
page, save_id, cancel_id = create_interactive_demo_page()
|
|
|
|
# Create state manager
|
|
state_mgr = InteractionStateManager(page)
|
|
|
|
# Initial render
|
|
print("Initial render:")
|
|
current_frame = page.render()
|
|
print(f" ✓ Rendered initial state")
|
|
print()
|
|
|
|
# Simulate cursor moving over a button
|
|
button_center = (150, 150)
|
|
print(f"Cursor moves to button position {button_center}:")
|
|
hover_frame = state_mgr.update_hover(button_center)
|
|
if hover_frame:
|
|
print(f" ✓ Hover state changed, page re-rendered")
|
|
hover_frame.save("demo_07_hover.png")
|
|
print(f" ✓ Saved: demo_07_hover.png")
|
|
print()
|
|
|
|
# Simulate mouse down
|
|
print(f"Mouse down at {button_center}:")
|
|
pressed_frame = state_mgr.handle_mouse_down(button_center)
|
|
if pressed_frame:
|
|
print(f" ✓ Pressed state set, page re-rendered")
|
|
pressed_frame.save("demo_07_state_mgr_pressed.png")
|
|
print(f" ✓ Saved: demo_07_state_mgr_pressed.png")
|
|
print()
|
|
|
|
# Wait for visual feedback
|
|
time.sleep(0.15)
|
|
|
|
# Simulate mouse up
|
|
print(f"Mouse up at {button_center}:")
|
|
released_frame, result = state_mgr.handle_mouse_up(button_center)
|
|
if released_frame:
|
|
print(f" ✓ Released state set, page re-rendered")
|
|
print(f" ✓ Callback result: {result}")
|
|
released_frame.save("demo_07_state_mgr_released.png")
|
|
print(f" ✓ Saved: demo_07_state_mgr_released.png")
|
|
print()
|
|
|
|
# Simulate cursor moving away
|
|
away_point = (50, 50)
|
|
print(f"Cursor moves away to {away_point}:")
|
|
away_frame = state_mgr.update_hover(away_point)
|
|
if away_frame:
|
|
print(f" ✓ Hover state cleared, page re-rendered")
|
|
away_frame.save("demo_07_no_hover.png")
|
|
print(f" ✓ Saved: demo_07_no_hover.png")
|
|
print()
|
|
|
|
|
|
def demo_performance_optimization():
|
|
"""
|
|
Demonstrate how the dirty flag prevents unnecessary re-renders.
|
|
"""
|
|
print("=" * 70)
|
|
print("Demo 4: Performance Optimization with Dirty Flag")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Create the page
|
|
page, save_id, cancel_id = create_interactive_demo_page()
|
|
|
|
print("Scenario: Multiple state queries without changes")
|
|
print()
|
|
|
|
# Initial render
|
|
page.render()
|
|
print(f"1. After initial render - dirty: {page.is_dirty}")
|
|
|
|
# Check if dirty before rendering again
|
|
print(f"2. Check dirty flag: {page.is_dirty}")
|
|
if not page.is_dirty:
|
|
print(" → Skipping render (no changes)")
|
|
print()
|
|
|
|
# Now make a change
|
|
button = page.callbacks.get_by_id("save-btn")
|
|
print("3. Setting button to pressed state")
|
|
# Ensure page reference is set
|
|
if not hasattr(button, '_page') or button._page is None:
|
|
button.set_page(page)
|
|
button.set_pressed(True)
|
|
print(f" → dirty: {page.is_dirty}")
|
|
print()
|
|
|
|
# This time we need to render
|
|
print(f"4. Check dirty flag: {page.is_dirty}")
|
|
if page.is_dirty:
|
|
print(" → Re-rendering (state changed)")
|
|
page.render()
|
|
print(f" → dirty after render: {page.is_dirty}")
|
|
print()
|
|
|
|
print("Benefit: Only render when actual changes occur!")
|
|
print()
|
|
|
|
|
|
def create_animated_gif():
|
|
"""
|
|
Create an animated GIF showing the button press sequence.
|
|
"""
|
|
from PIL import Image
|
|
import os
|
|
|
|
print("=" * 70)
|
|
print("Creating Animated GIF")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Check if the PNG files exist
|
|
png_files = [
|
|
"demo_07_initial.png",
|
|
"demo_07_pressed.png",
|
|
"demo_07_released.png"
|
|
]
|
|
|
|
if not all(os.path.exists(f) for f in png_files):
|
|
print(" ⚠ PNG files not found, skipping GIF creation")
|
|
return
|
|
|
|
# Load the images
|
|
initial = Image.open('demo_07_initial.png')
|
|
pressed = Image.open('demo_07_pressed.png')
|
|
released = Image.open('demo_07_released.png')
|
|
|
|
# Create animated GIF showing the button interaction sequence
|
|
# Sequence: initial (1000ms) -> pressed (200ms) -> released (500ms) -> loop
|
|
frames = [initial, pressed, released]
|
|
durations = [1000, 200, 500] # milliseconds per frame
|
|
|
|
output_path = "docs/images/example_07_button_animation.gif"
|
|
|
|
# Create docs/images directory if it doesn't exist
|
|
os.makedirs("docs/images", exist_ok=True)
|
|
|
|
# Save as animated GIF
|
|
initial.save(
|
|
output_path,
|
|
save_all=True,
|
|
append_images=[pressed, released],
|
|
duration=durations,
|
|
loop=0 # 0 means loop forever
|
|
)
|
|
|
|
print(f" ✓ Created: {output_path}")
|
|
print(f" ✓ Frames: {len(frames)}")
|
|
print(f" ✓ Sequence: initial (1000ms) → pressed (200ms) → released (500ms)")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("\n")
|
|
print("╔" + "═" * 68 + "╗")
|
|
print("║" + " " * 15 + "PRESSED STATE DEMONSTRATION" + " " * 26 + "║")
|
|
print("╚" + "═" * 68 + "╝")
|
|
print()
|
|
|
|
# Run all demos
|
|
demo_automatic_interaction()
|
|
print("\n")
|
|
|
|
demo_manual_state_management()
|
|
print("\n")
|
|
|
|
demo_state_manager()
|
|
print("\n")
|
|
|
|
demo_performance_optimization()
|
|
print("\n")
|
|
|
|
# Create animated GIF
|
|
create_animated_gif()
|
|
|
|
print("=" * 70)
|
|
print("All demos complete! Check the generated PNG files and animated GIF.")
|
|
print("=" * 70)
|