320 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			320 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
#!/usr/bin/env python3
 | 
						|
"""
 | 
						|
Text Input Widget System for McRogueFace
 | 
						|
A pure Python implementation of focusable text input fields
 | 
						|
"""
 | 
						|
 | 
						|
import mcrfpy
 | 
						|
import sys
 | 
						|
from dataclasses import dataclass
 | 
						|
from typing import Optional, List, Callable
 | 
						|
 | 
						|
 | 
						|
class FocusManager:
 | 
						|
    """Manages focus state across multiple widgets"""
 | 
						|
    def __init__(self):
 | 
						|
        self.widgets: List['TextInput'] = []
 | 
						|
        self.focused_widget: Optional['TextInput'] = None
 | 
						|
        self.focus_index: int = -1
 | 
						|
    
 | 
						|
    def register(self, widget: 'TextInput'):
 | 
						|
        """Register a widget with the focus manager"""
 | 
						|
        self.widgets.append(widget)
 | 
						|
        if self.focused_widget is None:
 | 
						|
            self.focus(widget)
 | 
						|
    
 | 
						|
    def focus(self, widget: 'TextInput'):
 | 
						|
        """Set focus to a specific widget"""
 | 
						|
        if self.focused_widget:
 | 
						|
            self.focused_widget.on_blur()
 | 
						|
        
 | 
						|
        self.focused_widget = widget
 | 
						|
        self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
 | 
						|
        
 | 
						|
        if widget:
 | 
						|
            widget.on_focus()
 | 
						|
    
 | 
						|
    def focus_next(self):
 | 
						|
        """Focus the next widget in the list"""
 | 
						|
        if not self.widgets:
 | 
						|
            return
 | 
						|
        
 | 
						|
        self.focus_index = (self.focus_index + 1) % len(self.widgets)
 | 
						|
        self.focus(self.widgets[self.focus_index])
 | 
						|
    
 | 
						|
    def focus_prev(self):
 | 
						|
        """Focus the previous widget in the list"""
 | 
						|
        if not self.widgets:
 | 
						|
            return
 | 
						|
        
 | 
						|
        self.focus_index = (self.focus_index - 1) % len(self.widgets)
 | 
						|
        self.focus(self.widgets[self.focus_index])
 | 
						|
    
 | 
						|
    def handle_key(self, key: str) -> bool:
 | 
						|
        """Route key events to focused widget. Returns True if handled."""
 | 
						|
        if self.focused_widget:
 | 
						|
            return self.focused_widget.handle_key(key)
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
class TextInput:
 | 
						|
    """A text input widget with cursor and selection support"""
 | 
						|
    def __init__(self, x: int, y: int, width: int = 200, label: str = "", 
 | 
						|
                 font_size: int = 16, on_change: Optional[Callable] = None):
 | 
						|
        self.x = x
 | 
						|
        self.y = y
 | 
						|
        self.width = width
 | 
						|
        self.label = label
 | 
						|
        self.font_size = font_size
 | 
						|
        self.on_change = on_change
 | 
						|
        
 | 
						|
        # Text state
 | 
						|
        self.text = ""
 | 
						|
        self.cursor_pos = 0
 | 
						|
        self.selection_start = -1
 | 
						|
        self.selection_end = -1
 | 
						|
        
 | 
						|
        # Visual state
 | 
						|
        self.focused = False
 | 
						|
        self.cursor_visible = True
 | 
						|
        self.cursor_blink_timer = 0
 | 
						|
        
 | 
						|
        # Create UI elements
 | 
						|
        self._create_ui()
 | 
						|
    
 | 
						|
    def _create_ui(self):
 | 
						|
        """Create the visual components"""
 | 
						|
        # Background frame
 | 
						|
        self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.font_size + 8)
 | 
						|
        self.frame.outline = 2
 | 
						|
        self.frame.fill_color = (255, 255, 255, 255)
 | 
						|
        self.frame.outline_color = (128, 128, 128, 255)
 | 
						|
        
 | 
						|
        # Label (if provided)
 | 
						|
        if self.label:
 | 
						|
            self.label_text = mcrfpy.Caption(
 | 
						|
                self.x - 5, 
 | 
						|
                self.y - self.font_size - 5,
 | 
						|
                self.label
 | 
						|
            )
 | 
						|
            self.label_text.color = (255, 255, 255, 255)
 | 
						|
        
 | 
						|
        # Text display
 | 
						|
        self.text_display = mcrfpy.Caption(
 | 
						|
            self.x + 4,
 | 
						|
            self.y + 4,
 | 
						|
            ""
 | 
						|
        )
 | 
						|
        self.text_display.color = (0, 0, 0, 255)
 | 
						|
        
 | 
						|
        # Cursor (using a thin frame)
 | 
						|
        self.cursor = mcrfpy.Frame(
 | 
						|
            self.x + 4,
 | 
						|
            self.y + 4,
 | 
						|
            2,
 | 
						|
            self.font_size
 | 
						|
        )
 | 
						|
        self.cursor.fill_color = (0, 0, 0, 255)
 | 
						|
        self.cursor.visible = False
 | 
						|
        
 | 
						|
        # Click handler
 | 
						|
        self.frame.click = self._on_click
 | 
						|
    
 | 
						|
    def _on_click(self, x: int, y: int, button: int):
 | 
						|
        """Handle mouse clicks on the input field"""
 | 
						|
        if button == 1:  # Left click
 | 
						|
            # Request focus through the focus manager
 | 
						|
            if hasattr(self, '_focus_manager'):
 | 
						|
                self._focus_manager.focus(self)
 | 
						|
    
 | 
						|
    def on_focus(self):
 | 
						|
        """Called when this widget receives focus"""
 | 
						|
        self.focused = True
 | 
						|
        self.frame.outline_color = (0, 120, 255, 255)
 | 
						|
        self.frame.outline = 3
 | 
						|
        self.cursor.visible = True
 | 
						|
        self._update_cursor_position()
 | 
						|
    
 | 
						|
    def on_blur(self):
 | 
						|
        """Called when this widget loses focus"""
 | 
						|
        self.focused = False
 | 
						|
        self.frame.outline_color = (128, 128, 128, 255)
 | 
						|
        self.frame.outline = 2
 | 
						|
        self.cursor.visible = False
 | 
						|
    
 | 
						|
    def handle_key(self, key: str) -> bool:
 | 
						|
        """Handle keyboard input. Returns True if key was handled."""
 | 
						|
        if not self.focused:
 | 
						|
            return False
 | 
						|
        
 | 
						|
        handled = True
 | 
						|
        old_text = self.text
 | 
						|
        
 | 
						|
        # Special keys
 | 
						|
        if key == "BackSpace":
 | 
						|
            if self.cursor_pos > 0:
 | 
						|
                self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
 | 
						|
                self.cursor_pos -= 1
 | 
						|
        elif key == "Delete":
 | 
						|
            if self.cursor_pos < len(self.text):
 | 
						|
                self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
 | 
						|
        elif key == "Left":
 | 
						|
            self.cursor_pos = max(0, self.cursor_pos - 1)
 | 
						|
        elif key == "Right":
 | 
						|
            self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
 | 
						|
        elif key == "Home":
 | 
						|
            self.cursor_pos = 0
 | 
						|
        elif key == "End":
 | 
						|
            self.cursor_pos = len(self.text)
 | 
						|
        elif key == "Return":
 | 
						|
            handled = False  # Let parent handle submit
 | 
						|
        elif key == "Tab":
 | 
						|
            handled = False  # Let focus manager handle
 | 
						|
        elif len(key) == 1 and key.isprintable():
 | 
						|
            # Regular character input
 | 
						|
            self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
 | 
						|
            self.cursor_pos += 1
 | 
						|
        else:
 | 
						|
            handled = False
 | 
						|
        
 | 
						|
        # Update display
 | 
						|
        if old_text != self.text:
 | 
						|
            self._update_display()
 | 
						|
            if self.on_change:
 | 
						|
                self.on_change(self.text)
 | 
						|
        else:
 | 
						|
            self._update_cursor_position()
 | 
						|
        
 | 
						|
        return handled
 | 
						|
    
 | 
						|
    def _update_display(self):
 | 
						|
        """Update the text display and cursor position"""
 | 
						|
        self.text_display.text = self.text
 | 
						|
        self._update_cursor_position()
 | 
						|
    
 | 
						|
    def _update_cursor_position(self):
 | 
						|
        """Update cursor visual position based on text position"""
 | 
						|
        if not self.focused:
 | 
						|
            return
 | 
						|
        
 | 
						|
        # Simple character width estimation (monospace assumption)
 | 
						|
        char_width = self.font_size * 0.6
 | 
						|
        cursor_x = self.x + 4 + int(self.cursor_pos * char_width)
 | 
						|
        self.cursor.x = cursor_x
 | 
						|
    
 | 
						|
    def set_text(self, text: str):
 | 
						|
        """Set the text content"""
 | 
						|
        self.text = text
 | 
						|
        self.cursor_pos = len(text)
 | 
						|
        self._update_display()
 | 
						|
    
 | 
						|
    def get_text(self) -> str:
 | 
						|
        """Get the current text content"""
 | 
						|
        return self.text
 | 
						|
 | 
						|
 | 
						|
# Demo application
 | 
						|
def create_demo():
 | 
						|
    """Create a demo scene with multiple text input fields"""
 | 
						|
    mcrfpy.createScene("text_input_demo")
 | 
						|
    scene = mcrfpy.sceneUI("text_input_demo")
 | 
						|
    
 | 
						|
    # Create background
 | 
						|
    bg = mcrfpy.Frame(0, 0, 800, 600)
 | 
						|
    bg.fill_color = (40, 40, 40, 255)
 | 
						|
    scene.append(bg)
 | 
						|
    
 | 
						|
    # Title
 | 
						|
    title = mcrfpy.Caption(10, 10, "Text Input Widget Demo")
 | 
						|
    title.color = (255, 255, 255, 255)
 | 
						|
    scene.append(title)
 | 
						|
    
 | 
						|
    # Instructions
 | 
						|
    instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text")
 | 
						|
    instructions.color = (200, 200, 200, 255)
 | 
						|
    scene.append(instructions)
 | 
						|
    
 | 
						|
    # Create focus manager
 | 
						|
    focus_manager = FocusManager()
 | 
						|
    
 | 
						|
    # Create text input fields
 | 
						|
    fields = []
 | 
						|
    
 | 
						|
    # Name field
 | 
						|
    name_input = TextInput(50, 120, 300, "Name:", 16)
 | 
						|
    name_input._focus_manager = focus_manager
 | 
						|
    focus_manager.register(name_input)
 | 
						|
    scene.append(name_input.frame)
 | 
						|
    if hasattr(name_input, 'label_text'):
 | 
						|
        scene.append(name_input.label_text)
 | 
						|
    scene.append(name_input.text_display)
 | 
						|
    scene.append(name_input.cursor)
 | 
						|
    fields.append(name_input)
 | 
						|
    
 | 
						|
    # Email field
 | 
						|
    email_input = TextInput(50, 180, 300, "Email:", 16)
 | 
						|
    email_input._focus_manager = focus_manager
 | 
						|
    focus_manager.register(email_input)
 | 
						|
    scene.append(email_input.frame)
 | 
						|
    if hasattr(email_input, 'label_text'):
 | 
						|
        scene.append(email_input.label_text)
 | 
						|
    scene.append(email_input.text_display)
 | 
						|
    scene.append(email_input.cursor)
 | 
						|
    fields.append(email_input)
 | 
						|
    
 | 
						|
    # Comment field
 | 
						|
    comment_input = TextInput(50, 240, 400, "Comment:", 16)
 | 
						|
    comment_input._focus_manager = focus_manager
 | 
						|
    focus_manager.register(comment_input)
 | 
						|
    scene.append(comment_input.frame)
 | 
						|
    if hasattr(comment_input, 'label_text'):
 | 
						|
        scene.append(comment_input.label_text)
 | 
						|
    scene.append(comment_input.text_display)
 | 
						|
    scene.append(comment_input.cursor)
 | 
						|
    fields.append(comment_input)
 | 
						|
    
 | 
						|
    # Result display
 | 
						|
    result_text = mcrfpy.Caption(50, 320, "Type in the fields above...")
 | 
						|
    result_text.color = (150, 255, 150, 255)
 | 
						|
    scene.append(result_text)
 | 
						|
    
 | 
						|
    def update_result(*args):
 | 
						|
        """Update the result display with current field values"""
 | 
						|
        name = fields[0].get_text()
 | 
						|
        email = fields[1].get_text()
 | 
						|
        comment = fields[2].get_text()
 | 
						|
        result_text.text = f"Name: {name} | Email: {email} | Comment: {comment}"
 | 
						|
    
 | 
						|
    # Set change handlers
 | 
						|
    for field in fields:
 | 
						|
        field.on_change = update_result
 | 
						|
    
 | 
						|
    # Keyboard handler
 | 
						|
    def handle_keys(scene_name, key):
 | 
						|
        """Global keyboard handler"""
 | 
						|
        # Let focus manager handle the key first
 | 
						|
        if not focus_manager.handle_key(key):
 | 
						|
            # Handle focus switching
 | 
						|
            if key == "Tab":
 | 
						|
                focus_manager.focus_next()
 | 
						|
            elif key == "Escape":
 | 
						|
                print("Demo complete!")
 | 
						|
                sys.exit(0)
 | 
						|
    
 | 
						|
    mcrfpy.keypressScene("text_input_demo", handle_keys)
 | 
						|
    
 | 
						|
    # Set the scene
 | 
						|
    mcrfpy.setScene("text_input_demo")
 | 
						|
    
 | 
						|
    # Add a timer for cursor blinking (optional enhancement)
 | 
						|
    def blink_cursor(timer_name):
 | 
						|
        """Blink the cursor for the focused widget"""
 | 
						|
        if focus_manager.focused_widget and focus_manager.focused_widget.focused:
 | 
						|
            cursor = focus_manager.focused_widget.cursor
 | 
						|
            cursor.visible = not cursor.visible
 | 
						|
    
 | 
						|
    mcrfpy.setTimer("cursor_blink", blink_cursor, 500)  # Blink every 500ms
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    create_demo() |