DynamicAbstractionSystem/ui/layout_manager.py

597 lines
21 KiB
Python

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