""" 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)