pyWebLayout/examples/07_pressed_state_demo.py
2025-11-12 12:03:27 +00:00

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)