"""Layout management system with constraint-based positioning and event-driven updates.""" from typing import Dict, List, Optional, Tuple, Callable, Any from dataclasses import dataclass, field from enum import Enum import math import time from core.event_bus import EventBus, EventType, Event, PanelEvent, LayoutEvent class LayoutType(Enum): """Layout types for containers.""" HORIZONTAL = "horizontal" VERTICAL = "vertical" GRID = "grid" ABSOLUTE = "absolute" class ResizePolicy(Enum): """Resize policies for panels.""" FIXED = "fixed" FLEXIBLE = "flexible" PROPORTIONAL = "proportional" AUTO = "auto" @dataclass class LayoutConstraints: """Layout constraints for a panel.""" min_width: float = 0 min_height: float = 0 max_width: float = float('inf') max_height: float = float('inf') preferred_width: float = 0 preferred_height: float = 0 flex_width: float = 1.0 flex_height: float = 1.0 resize_policy: ResizePolicy = ResizePolicy.FLEXIBLE def apply_constraints(self, width: float, height: float) -> Tuple[float, float]: """Apply size constraints to a given dimension.""" width = max(self.min_width, min(self.max_width, width)) height = max(self.min_height, min(self.max_height, height)) return width, height @dataclass class PanelState: """State information for a panel.""" panel_id: str x: float = 0 y: float = 0 width: float = 100 height: float = 100 visible: bool = True focused: bool = False constraints: LayoutConstraints = field(default_factory=LayoutConstraints) parent: Optional[str] = None children: List[str] = field(default_factory=list) def get_rect(self) -> Tuple[float, float, float, float]: """Get panel rectangle as (x, y, width, height).""" return self.x, self.y, self.width, self.height def contains_point(self, x: float, y: float) -> bool: """Check if a point is inside the panel.""" return (self.x <= x <= self.x + self.width and self.y <= y <= self.y + self.height) @dataclass class ContainerState: """State information for a layout container.""" container_id: str layout_type: LayoutType = LayoutType.HORIZONTAL x: float = 0 y: float = 0 width: float = 100 height: float = 100 padding: float = 0 spacing: float = 0 panels: List[str] = field(default_factory=list) parent: Optional[str] = None def get_rect(self) -> Tuple[float, float, float, float]: """Get container rectangle as (x, y, width, height).""" return self.x, self.y, self.width, self.height @dataclass class AnimationState: """Animation state for smooth transitions.""" target_x: float = 0 target_y: float = 0 target_width: float = 0 target_height: float = 0 start_x: float = 0 start_y: float = 0 start_width: float = 0 start_height: float = 0 progress: float = 0 duration: float = 0.2 easing: str = "ease_out_quad" active: bool = False class LayoutManager: """Enhanced layout manager with constraint-based positioning and animations.""" def __init__(self, event_bus: Optional[EventBus] = None): self.event_bus = event_bus self.panels: Dict[str, PanelState] = {} self.containers: Dict[str, ContainerState] = {} self.animations: Dict[str, AnimationState] = {} # Root container self.root_container = ContainerState( container_id="root", layout_type=LayoutType.VERTICAL ) self.containers["root"] = self.root_container # Layout preferences self.animation_enabled = True self.auto_layout = True # Performance tracking self.last_layout_time = 0 self.layout_count = 0 # Subscribe to events if available if self.event_bus: self._setup_event_subscriptions() def _setup_event_subscriptions(self): """Setup event subscriptions for layout management.""" self.event_bus.subscribe(EventType.PANEL_RESIZE, self._on_panel_resize) self.event_bus.subscribe(EventType.PANEL_FOCUS, self._on_panel_focus) self.event_bus.subscribe(EventType.LAYOUT_CHANGE, self._on_layout_change) self.event_bus.subscribe(EventType.VIEWPORT_UPDATE, self._on_viewport_update) def add_panel(self, panel_id: str, x: float = 0, y: float = 0, width: float = 100, height: float = 100, constraints: Optional[LayoutConstraints] = None, container_id: Optional[str] = None) -> PanelState: """Add a new panel to the layout system.""" panel = PanelState( panel_id=panel_id, x=x, y=y, width=width, height=height, constraints=constraints or LayoutConstraints() ) self.panels[panel_id] = panel # Add to container if specified if container_id and container_id in self.containers: self.containers[container_id].panels.append(panel_id) panel.parent = container_id else: # Add to root by default self.root_container.panels.append(panel_id) panel.parent = "root" # Publish event if self.event_bus: self.event_bus.create_panel_event( EventType.PANEL_RESIZE, panel_id, x, y, width, height, source="LayoutManager.add_panel" ) return panel def add_container(self, container_id: str, layout_type: LayoutType, x: float = 0, y: float = 0, width: float = 100, height: float = 100, parent_container: Optional[str] = None) -> ContainerState: """Add a new layout container.""" container = ContainerState( container_id=container_id, layout_type=layout_type, x=x, y=y, width=width, height=height ) self.containers[container_id] = container # Set parent relationship if parent_container and parent_container in self.containers: self.containers[parent_container].panels.append(container_id) container.parent = parent_container return container def resize_panel(self, panel_id: str, width: float, height: float, animate: bool = True) -> bool: """Resize a panel with optional animation.""" if panel_id not in self.panels: return False panel = self.panels[panel_id] # Apply constraints width, height = panel.constraints.apply_constraints(width, height) if animate and self.animation_enabled: # Start animation if panel_id not in self.animations: self.animations[panel_id] = AnimationState() anim = self.animations[panel_id] anim.start_x, anim.start_y = panel.x, panel.y anim.start_width, anim.start_height = panel.width, panel.height anim.target_x, anim.target_y = panel.x, panel.y anim.target_width, anim.target_height = width, height anim.progress = 0 anim.active = True else: # Apply immediately panel.width, panel.height = width, height panel.x, panel.y = panel.x, panel.y # Keep position # Publish event if self.event_bus: self.event_bus.create_panel_event( EventType.PANEL_RESIZE, panel_id, panel.x, panel.y, panel.width, panel.height, source="LayoutManager.resize_panel" ) return True def move_panel(self, panel_id: str, x: float, y: float, animate: bool = True) -> bool: """Move a panel with optional animation.""" if panel_id not in self.panels: return False panel = self.panels[panel_id] if animate and self.animation_enabled: # Start animation if panel_id not in self.animations: self.animations[panel_id] = AnimationState() anim = self.animations[panel_id] anim.start_x, anim.start_y = panel.x, panel.y anim.start_width, anim.start_height = panel.width, panel.height anim.target_x, anim.target_y = x, y anim.target_width, anim.target_height = panel.width, panel.height anim.progress = 0 anim.active = True else: # Apply immediately panel.x, panel.y = x, y # Publish event if self.event_bus: self.event_bus.create_panel_event( EventType.PANEL_RESIZE, panel_id, panel.x, panel.y, panel.width, panel.height, source="LayoutManager.move_panel" ) return True def set_panel_constraints(self, panel_id: str, constraints: LayoutConstraints) -> bool: """Set layout constraints for a panel.""" if panel_id not in self.panels: return False self.panels[panel_id].constraints = constraints return True def update_layout(self, container_id: Optional[str] = None) -> bool: """Update layout calculations for a container or all containers.""" start_time = time.time() if container_id: containers_to_update = [container_id] if container_id in self.containers else [] else: containers_to_update = list(self.containers.keys()) for container_id in containers_to_update: self._update_container_layout(container_id) # Update animations self._update_animations() # Track performance self.last_layout_time = time.time() - start_time self.layout_count += 1 return True def _update_container_layout(self, container_id: str): """Update layout for a specific container.""" if container_id not in self.containers: return container = self.containers[container_id] if container.layout_type == LayoutType.HORIZONTAL: self._layout_horizontal(container) elif container.layout_type == LayoutType.VERTICAL: self._layout_vertical(container) elif container.layout_type == LayoutType.GRID: self._layout_grid(container) # ABSOLUTE layout doesn't need automatic positioning def _layout_horizontal(self, container: ContainerState): """Layout panels horizontally within a container.""" available_width = container.width - (2 * container.padding) total_flex = 0 fixed_width_total = 0 # Calculate flex and fixed widths panel_ids = [p for p in container.panels if p in self.panels] for panel_id in panel_ids: panel = self.panels[panel_id] if not panel.visible: continue if panel.constraints.resize_policy == ResizePolicy.FIXED: fixed_width_total += panel.constraints.preferred_width else: total_flex += panel.constraints.flex_width # Add spacing to fixed total if len(panel_ids) > 1: fixed_width_total += (len(panel_ids) - 1) * container.spacing available_flex_width = max(0, available_width - fixed_width_total) # Position panels current_x = container.x + container.padding for panel_id in panel_ids: panel = self.panels[panel_id] if not panel.visible: continue if panel.constraints.resize_policy == ResizePolicy.FIXED: width = panel.constraints.preferred_width else: if total_flex > 0: width = available_flex_width * (panel.constraints.flex_width / total_flex) else: width = panel.width # Apply constraints width, height = panel.constraints.apply_constraints(width, container.height) panel.x = current_x panel.y = container.y + container.padding panel.width = width panel.height = height current_x += width + container.spacing def _layout_vertical(self, container: ContainerState): """Layout panels vertically within a container.""" available_height = container.height - (2 * container.padding) total_flex = 0 fixed_height_total = 0 # Calculate flex and fixed heights panel_ids = [p for p in container.panels if p in self.panels] for panel_id in panel_ids: panel = self.panels[panel_id] if not panel.visible: continue if panel.constraints.resize_policy == ResizePolicy.FIXED: fixed_height_total += panel.constraints.preferred_height else: total_flex += panel.constraints.flex_height # Add spacing to fixed total if len(panel_ids) > 1: fixed_height_total += (len(panel_ids) - 1) * container.spacing available_flex_height = max(0, available_height - fixed_height_total) # Position panels current_y = container.y + container.padding for panel_id in panel_ids: panel = self.panels[panel_id] if not panel.visible: continue if panel.constraints.resize_policy == ResizePolicy.FIXED: height = panel.constraints.preferred_height else: if total_flex > 0: height = available_flex_height * (panel.constraints.flex_height / total_flex) else: height = panel.height # Apply constraints width, height = panel.constraints.apply_constraints(container.width, height) panel.x = container.x + container.padding panel.y = current_y panel.width = width panel.height = height current_y += height + container.spacing def _layout_grid(self, container: ContainerState): """Layout panels in a grid within a container.""" # Simple grid layout - can be enhanced with more sophisticated grid logic panel_ids = [p for p in container.panels if p in self.panels] if not panel_ids: return # Calculate grid dimensions (for now, use a simple 2xN grid) cols = min(2, len(panel_ids)) rows = math.ceil(len(panel_ids) / cols) cell_width = (container.width - (2 * container.padding) - (cols - 1) * container.spacing) / cols cell_height = (container.height - (2 * container.padding) - (rows - 1) * container.spacing) / rows for i, panel_id in enumerate(panel_ids): panel = self.panels[panel_id] if not panel.visible: continue col = i % cols row = i // cols panel.x = container.x + container.padding + col * (cell_width + container.spacing) panel.y = container.y + container.padding + row * (cell_height + container.spacing) panel.width, panel.height = panel.constraints.apply_constraints(cell_width, cell_height) def _update_animations(self): """Update active animations.""" if not self.animation_enabled: return dt = 0.016 # Assume 60 FPS for now completed_animations = [] for panel_id, anim in self.animations.items(): if not anim.active or panel_id not in self.panels: continue anim.progress += dt / anim.duration if anim.progress >= 1.0: anim.progress = 1.0 anim.active = False completed_animations.append(panel_id) # Apply easing t = self._apply_easing(anim.progress, anim.easing) # Interpolate position and size panel = self.panels[panel_id] panel.x = anim.start_x + (anim.target_x - anim.start_x) * t panel.y = anim.start_y + (anim.target_y - anim.start_y) * t panel.width = anim.start_width + (anim.target_width - anim.start_width) * t panel.height = anim.start_height + (anim.target_height - anim.start_height) * t # Publish update event if self.event_bus: self.event_bus.create_panel_event( EventType.PANEL_RESIZE, panel_id, panel.x, panel.y, panel.width, panel.height, source="LayoutManager.animation" ) # Clean up completed animations for panel_id in completed_animations: del self.animations[panel_id] def _apply_easing(self, t: float, easing_type: str) -> float: """Apply easing function to animation progress.""" if easing_type == "linear": return t elif easing_type == "ease_in_quad": return t * t elif easing_type == "ease_out_quad": return 1 - (1 - t) * (1 - t) elif easing_type == "ease_in_out_quad": return 2 * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 2) / 2 else: return t # Default to linear def get_panel_at_position(self, x: float, y: float) -> Optional[str]: """Get the panel at a given position.""" # Check panels in reverse order (top to bottom) for panel_id, panel in reversed(list(self.panels.items())): if panel.visible and panel.contains_point(x, y): return panel_id return None def get_panel_rect(self, panel_id: str) -> Optional[Tuple[float, float, float, float]]: """Get the rectangle of a panel.""" if panel_id in self.panels: return self.panels[panel_id].get_rect() return None def remove_panel(self, panel_id: str) -> bool: """Remove a panel from the layout system.""" if panel_id not in self.panels: return False panel = self.panels[panel_id] # Remove from parent container if panel.parent and panel.parent in self.containers: container = self.containers[panel.parent] if panel_id in container.panels: container.panels.remove(panel_id) # Remove panel and any animations del self.panels[panel_id] if panel_id in self.animations: del self.animations[panel_id] return True def set_viewport_size(self, width: float, height: float): """Update the root container size.""" self.root_container.width = width self.root_container.height = height if self.event_bus: self.event_bus.create_event( EventType.VIEWPORT_UPDATE, {"width": width, "height": height}, source="LayoutManager.set_viewport_size" ) if self.auto_layout: self.update_layout() def focus_panel(self, panel_id: str) -> bool: """Set focus to a panel.""" if panel_id not in self.panels: return False # Clear focus from all panels for panel in self.panels.values(): panel.focused = False # Set focus to target panel self.panels[panel_id].focused = True if self.event_bus: self.event_bus.create_event( EventType.PANEL_FOCUS, {"panel_id": panel_id, "focused": True}, source="LayoutManager.focus_panel" ) return True def get_layout_stats(self) -> Dict[str, Any]: """Get layout performance statistics.""" return { "panel_count": len(self.panels), "container_count": len(self.containers), "active_animations": len([a for a in self.animations.values() if a.active]), "last_layout_time": self.last_layout_time, "total_layouts": self.layout_count, "auto_layout_enabled": self.auto_layout, "animation_enabled": self.animation_enabled } # Event handlers def _on_panel_resize(self, event: PanelEvent): """Handle panel resize events.""" if event.panel_id in self.panels: panel = self.panels[event.panel_id] if not (hasattr(self, '_internal_update') and self._internal_update): panel.x, panel.y = event.x, event.y panel.width, panel.height = event.width, event.height def _on_panel_focus(self, event: Event): """Handle panel focus events.""" panel_id = event.get("panel_id") if panel_id: self.focus_panel(panel_id) def _on_layout_change(self, event: LayoutEvent): """Handle layout change events.""" if event.container_id in self.containers: container = self.containers[event.container_id] if event.layout_type: container.layout_type = LayoutType(event.layout_type) self.update_layout(event.container_id) def _on_viewport_update(self, event: Event): """Handle viewport update events.""" width = event.get("width") height = event.get("height") if width is not None and height is not None: self.set_viewport_size(width, height)