#!/usr/bin/env python3 """Drag and Drop (Frame) Demo - Sort colored frames into target bins Interactive controls: Mouse drag: Move frames ESC: Return to menu This demonstrates: - Frame drag and drop using on_click + on_move (Pythonic method override pattern) - Hit testing for drop targets - State tracking and validation """ import mcrfpy import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) class DraggableFrame(mcrfpy.Frame): """A frame that can be dragged around the screen. Uses Pythonic method override pattern - just define on_click and on_move methods directly, no need for self.on_click = self._on_click assignment. """ def __init__(self, pos, size, color, color_type): """ Args: pos: Initial position tuple (x, y) size: Size tuple (w, h) color: Fill color tuple (r, g, b) color_type: 'red' or 'blue' for sorting validation """ super().__init__(pos, size, fill_color=color, outline=2, outline_color=(255, 255, 255)) self.color_type = color_type self.dragging = False self.drag_offset = (0, 0) self.original_pos = pos # No need for self.on_click = self._on_click - just define on_click method below! def on_click(self, pos, button, action): """Handle click events for drag start/end. Args: pos: mcrfpy.Vector with x, y coordinates button: mcrfpy.MouseButton enum (LEFT, RIGHT, etc.) action: mcrfpy.InputState enum (PRESSED, RELEASED) """ if button != mcrfpy.MouseButton.LEFT: return if action == mcrfpy.InputState.PRESSED: # Begin dragging - calculate offset from frame origin self.dragging = True self.drag_offset = (pos.x - self.x, pos.y - self.y) elif action == mcrfpy.InputState.RELEASED: if self.dragging: self.dragging = False # Notify demo of drop if hasattr(self, 'on_drop_callback'): self.on_drop_callback(self) def on_move(self, pos): """Handle mouse movement for dragging. Args: pos: mcrfpy.Vector with x, y coordinates Note: #230 - on_move now only receives position, not button/action """ if self.dragging: self.x = pos.x - self.drag_offset[0] self.y = pos.y - self.drag_offset[1] class DragDropFrameDemo: """Demo showing frame drag and drop with sorting bins.""" def __init__(self): self.scene = mcrfpy.Scene("demo_drag_drop_frame") self.ui = self.scene.children self.draggables = [] self.setup() def setup(self): """Build the demo UI.""" # Background bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(30, 30, 35)) self.ui.append(bg) # Title title = mcrfpy.Caption( text="Drag & Drop: Sort by Color", pos=(512, 30), font_size=28, fill_color=(255, 255, 255) ) title.outline = 2 title.outline_color = (0, 0, 0) self.ui.append(title) # Score caption self.score_caption = mcrfpy.Caption( text="Sorted: 0 / 8", pos=(512, 70), font_size=20, fill_color=(200, 200, 200) ) self.ui.append(self.score_caption) # Target bins (bottom half) # Red bin on the left self.red_bin = mcrfpy.Frame( pos=(20, 500), size=(482, 248), fill_color=(96, 0, 0), outline=3, outline_color=(200, 50, 50) ) self.ui.append(self.red_bin) red_label = mcrfpy.Caption( text="RED BIN", pos=(261, 600), font_size=32, fill_color=(200, 100, 100) ) self.ui.append(red_label) # Blue bin on the right self.blue_bin = mcrfpy.Frame( pos=(522, 500), size=(482, 248), fill_color=(0, 0, 96), outline=3, outline_color=(50, 50, 200) ) self.ui.append(self.blue_bin) blue_label = mcrfpy.Caption( text="BLUE BIN", pos=(763, 600), font_size=32, fill_color=(100, 100, 200) ) self.ui.append(blue_label) # Create draggable frames (top half) # 4 red frames, 4 blue frames, arranged in 2 rows frame_size = (100, 80) spacing = 20 start_x = 100 start_y = 120 positions = [] for row in range(2): for col in range(4): x = start_x + col * (frame_size[0] + spacing + 80) y = start_y + row * (frame_size[1] + spacing + 40) positions.append((x, y)) # Interleave red and blue colors = [ ((255, 64, 64), 'red'), ((64, 64, 255), 'blue'), ((255, 64, 64), 'red'), ((64, 64, 255), 'blue'), ((64, 64, 255), 'blue'), ((255, 64, 64), 'red'), ((64, 64, 255), 'blue'), ((255, 64, 64), 'red'), ] for i, (pos, (color, color_type)) in enumerate(zip(positions, colors)): frame = DraggableFrame(pos, frame_size, color, color_type) frame.on_drop_callback = self._on_frame_drop self.draggables.append(frame) self.ui.append(frame) # Add label inside frame label = mcrfpy.Caption( text=f"{i+1}", pos=(40, 25), font_size=24, fill_color=(255, 255, 255) ) frame.children.append(label) # Instructions instr = mcrfpy.Caption( text="Drag red frames to red bin, blue frames to blue bin | ESC to exit", pos=(512, 470), font_size=14, fill_color=(150, 150, 150) ) self.ui.append(instr) # Initial score update self._update_score() def _point_in_frame(self, x, y, frame): """Check if point (x, y) is inside frame.""" return (frame.x <= x <= frame.x + frame.w and frame.y <= y <= frame.y + frame.h) def _frame_in_bin(self, draggable, bin_frame): """Check if draggable frame's center is in bin.""" center_x = draggable.x + draggable.w / 2 center_y = draggable.y + draggable.h / 2 return self._point_in_frame(center_x, center_y, bin_frame) def _on_frame_drop(self, frame): """Called when a frame is dropped.""" self._update_score() def _update_score(self): """Count and display correctly sorted frames.""" correct = 0 for frame in self.draggables: if frame.color_type == 'red' and self._frame_in_bin(frame, self.red_bin): correct += 1 frame.outline_color = (0, 255, 0) # Green outline for correct elif frame.color_type == 'blue' and self._frame_in_bin(frame, self.blue_bin): correct += 1 frame.outline_color = (0, 255, 0) else: frame.outline_color = (255, 255, 255) # White outline otherwise self.score_caption.text = f"Sorted: {correct} / 8" if correct == 8: self.score_caption.text = "All Sorted! Well done!" self.score_caption.fill_color = (100, 255, 100) def on_key(self, key, state): """Handle keyboard input.""" if state != "start": return if key == "Escape": # Return to cookbook menu or exit try: from cookbook_main import main main() except: sys.exit(0) def activate(self): """Activate the demo scene.""" self.scene.on_key = self.on_key mcrfpy.current_scene = self.scene def main(): """Run the demo.""" demo = DragDropFrameDemo() demo.activate() # Headless screenshot try: if mcrfpy.headless_mode(): from mcrfpy import automation # Move some frames to bins for screenshot demo.draggables[0].x = 100 demo.draggables[0].y = 550 demo.draggables[1].x = 600 demo.draggables[1].y = 550 demo._update_score() mcrfpy.Timer("screenshot", lambda rt: ( automation.screenshot("screenshots/primitives/drag_drop_frame.png"), sys.exit(0) ), 100) except AttributeError: pass if __name__ == "__main__": main()