Implement tree widget for hierarchical entity inspection in HUD
This commit is contained in:
parent
849a39a528
commit
d90240391c
@ -60,6 +60,10 @@ class SimulationEngine:
|
||||
self.ui_manager = UIManager((self.window_width, self.window_height))
|
||||
self.hud = HUD(self.ui_manager, self.window_width, self.window_height)
|
||||
self.hud.update_layout(self.window_width, self.window_height)
|
||||
|
||||
# Initialize tree widget with the world
|
||||
self.hud.initialize_tree_widget(self.simulation_core.world)
|
||||
|
||||
self._update_simulation_view()
|
||||
|
||||
def _init_simulation(self):
|
||||
@ -188,9 +192,12 @@ class SimulationEngine:
|
||||
# Update selected objects in input handler
|
||||
self.input_handler.update_selected_objects()
|
||||
|
||||
# Sync tree selection with world selection
|
||||
self.hud.update_tree_selection(self.input_handler.selected_objects)
|
||||
|
||||
# Render frame
|
||||
self._update_frame(deltatime)
|
||||
self._render_frame()
|
||||
self._render_frame(deltatime)
|
||||
|
||||
def _sync_input_and_timing(self):
|
||||
"""Synchronize input handler state with simulation core timing."""
|
||||
@ -213,7 +220,7 @@ class SimulationEngine:
|
||||
keys = pygame.key.get_pressed()
|
||||
self.input_handler.update_camera(keys, deltatime)
|
||||
|
||||
def _render_frame(self):
|
||||
def _render_frame(self, deltatime):
|
||||
"""Render the complete frame."""
|
||||
self.screen.fill(BLACK)
|
||||
self.renderer.clear_screen()
|
||||
@ -222,7 +229,7 @@ class SimulationEngine:
|
||||
self._render_simulation_world()
|
||||
|
||||
# Update and render UI
|
||||
self._update_and_render_ui()
|
||||
self._update_and_render_ui(deltatime)
|
||||
|
||||
# Render HUD overlays
|
||||
self._render_hud_overlays()
|
||||
@ -251,15 +258,21 @@ class SimulationEngine:
|
||||
)
|
||||
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
|
||||
|
||||
def _update_and_render_ui(self):
|
||||
def _update_and_render_ui(self, deltatime):
|
||||
"""Update UI elements and render them."""
|
||||
# Update HUD displays with simulation core state
|
||||
self.hud.update_simulation_controls(self.simulation_core)
|
||||
|
||||
# Update tree widget
|
||||
self.hud.update_tree_widget(deltatime)
|
||||
|
||||
# Draw UI elements
|
||||
self.hud.manager.draw_ui(self.screen)
|
||||
self.hud.draw_splitters(self.screen)
|
||||
|
||||
# Render tree widget
|
||||
self.hud.render_tree_widget(self.screen)
|
||||
|
||||
def _render_hud_overlays(self):
|
||||
"""Render HUD overlay elements."""
|
||||
self.hud.render_fps(self.screen, self.clock)
|
||||
@ -290,6 +303,13 @@ class SimulationEngine:
|
||||
if not action:
|
||||
return
|
||||
|
||||
# Handle tree selection changes
|
||||
if isinstance(action, tuple) and action[0] == 'tree_selection_changed':
|
||||
selected_entities = action[1]
|
||||
# Sync world selection with tree selection
|
||||
self.input_handler.selected_objects = selected_entities
|
||||
return
|
||||
|
||||
# Handle simple actions directly
|
||||
if action in self._hud_action_handlers:
|
||||
self._hud_action_handlers[action]()
|
||||
|
||||
85
ui/hud.py
85
ui/hud.py
@ -7,6 +7,7 @@ from config.constants import *
|
||||
from world.base.brain import CellBrain, FlexibleNeuralNetwork
|
||||
from world.objects import DefaultCell
|
||||
from pygame_gui.elements import UIPanel, UIButton, UITextEntryLine, UILabel
|
||||
from ui.tree_widget import TreeWidget
|
||||
import math
|
||||
|
||||
DARK_GRAY = (40, 40, 40)
|
||||
@ -44,6 +45,10 @@ class HUD:
|
||||
self.custom_tps_entry = None
|
||||
self.tps_label = None
|
||||
|
||||
# Tree widget for inspector
|
||||
self.tree_widget = None
|
||||
self.world = None # Will be set when world is available
|
||||
|
||||
self._create_panels()
|
||||
self._create_simulation_controls()
|
||||
|
||||
@ -61,8 +66,9 @@ class HUD:
|
||||
else:
|
||||
self.control_bar = None
|
||||
|
||||
# Left inspector
|
||||
# Left inspector with tree widget
|
||||
if SHOW_INSPECTOR_PANEL:
|
||||
# Create a container panel for the inspector
|
||||
self.inspector_panel = UIPanel(
|
||||
relative_rect=pygame.Rect(
|
||||
0, self.control_bar_height if SHOW_CONTROL_BAR else 0,
|
||||
@ -73,8 +79,12 @@ class HUD:
|
||||
object_id="#inspector_panel",
|
||||
)
|
||||
self.panels.append(self.inspector_panel)
|
||||
|
||||
# Tree widget will be created when world is available
|
||||
self.tree_widget = None
|
||||
else:
|
||||
self.inspector_panel = None
|
||||
self.tree_widget = None
|
||||
|
||||
# Right properties
|
||||
if SHOW_PROPERTIES_PANEL:
|
||||
@ -110,6 +120,20 @@ class HUD:
|
||||
|
||||
self.dragging_splitter = None
|
||||
|
||||
def initialize_tree_widget(self, world):
|
||||
"""Initialize the tree widget when the world is available."""
|
||||
self.world = world
|
||||
|
||||
if self.inspector_panel and world:
|
||||
# Create tree widget inside the inspector panel
|
||||
tree_rect = pygame.Rect(0, 0, self.inspector_width, self.inspector_panel.rect.height)
|
||||
self.tree_widget = TreeWidget(tree_rect, self.manager, world)
|
||||
|
||||
def update_tree_selection(self, selected_objects):
|
||||
"""Update tree selection based on world selection."""
|
||||
if self.tree_widget:
|
||||
self.tree_widget.select_entities(selected_objects)
|
||||
|
||||
def _create_simulation_controls(self):
|
||||
"""Create simulation control buttons in the control bar."""
|
||||
if not self.control_bar:
|
||||
@ -216,6 +240,11 @@ class HUD:
|
||||
self.inspector_panel.set_relative_position((0, control_bar_height))
|
||||
self.inspector_panel.set_dimensions((self.inspector_width, self.screen_height - control_bar_height))
|
||||
|
||||
# Update tree widget size if it exists
|
||||
if self.tree_widget:
|
||||
tree_rect = pygame.Rect(0, 0, self.inspector_width, self.inspector_panel.rect.height)
|
||||
self.tree_widget.rect = tree_rect
|
||||
|
||||
# Properties panel (right)
|
||||
if self.properties_panel:
|
||||
self.properties_panel.set_relative_position(
|
||||
@ -300,6 +329,48 @@ class HUD:
|
||||
self.custom_tps_entry.set_text(str(int(timing_state.tps)))
|
||||
|
||||
def process_event(self, event):
|
||||
# Check for splitter dragging events first (don't let tree widget block them)
|
||||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
mx, my = event.pos
|
||||
# Check if mouse is on a splitter
|
||||
if abs(mx - self.inspector_width) < self.splitter_thickness:
|
||||
# Don't handle this event in the tree widget - it's for the splitter
|
||||
pass
|
||||
elif self.tree_widget and self.inspector_panel:
|
||||
# Handle tree widget events
|
||||
inspector_rect = self.inspector_panel.rect
|
||||
# The inspector_rect should be in absolute screen coordinates
|
||||
inspector_abs_x = inspector_rect.x
|
||||
inspector_abs_y = inspector_rect.y
|
||||
tree_local_pos = (event.pos[0] - inspector_abs_x, event.pos[1] - inspector_abs_y)
|
||||
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
|
||||
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
|
||||
event.pos = tree_local_pos
|
||||
if self.tree_widget.handle_event(event):
|
||||
selected_entities = self.tree_widget.get_selected_entities()
|
||||
return 'tree_selection_changed', selected_entities
|
||||
elif self.tree_widget and self.inspector_panel:
|
||||
# Handle other tree widget events
|
||||
if event.type in (pygame.MOUSEMOTION, pygame.MOUSEWHEEL):
|
||||
# Only handle if not dragging splitter
|
||||
if not self.dragging_splitter and hasattr(event, 'pos'):
|
||||
inspector_rect = self.inspector_panel.rect
|
||||
# Calculate absolute screen position (need to add control bar height)
|
||||
inspector_abs_x = inspector_rect.x
|
||||
inspector_abs_y = inspector_rect.y
|
||||
tree_local_pos = (event.pos[0] - inspector_abs_x, event.pos[1] - inspector_abs_y)
|
||||
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
|
||||
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
|
||||
event.pos = tree_local_pos
|
||||
if self.tree_widget.handle_event(event):
|
||||
selected_entities = self.tree_widget.get_selected_entities()
|
||||
return 'tree_selection_changed', selected_entities
|
||||
else:
|
||||
# For non-mouse events, try handling them normally
|
||||
if self.tree_widget.handle_event(event):
|
||||
selected_entities = self.tree_widget.get_selected_entities()
|
||||
return 'tree_selection_changed', selected_entities
|
||||
|
||||
# Handle simulation control button events using ID matching
|
||||
if event.type == pygame_gui.UI_BUTTON_START_PRESS:
|
||||
object_id = str(event.ui_object_id)
|
||||
@ -891,3 +962,15 @@ class HUD:
|
||||
screen.blit(tps_text, tps_rect)
|
||||
screen.blit(ticks_text, ticks_rect)
|
||||
screen.blit(cell_text, cell_rect)
|
||||
|
||||
def update_tree_widget(self, time_delta):
|
||||
"""Update the tree widget."""
|
||||
if self.tree_widget:
|
||||
self.tree_widget.update(time_delta)
|
||||
|
||||
def render_tree_widget(self, screen):
|
||||
"""Render the tree widget."""
|
||||
if self.tree_widget and self.inspector_panel:
|
||||
# Create a surface for the tree widget area
|
||||
tree_surface = screen.subsurface(self.inspector_panel.rect)
|
||||
self.tree_widget.draw(tree_surface)
|
||||
|
||||
274
ui/inspector_tree.py
Normal file
274
ui/inspector_tree.py
Normal file
@ -0,0 +1,274 @@
|
||||
"""
|
||||
Tree node classes for the hierarchical inspector view.
|
||||
Provides extensible tree structure for entity inspection.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any
|
||||
import pygame
|
||||
|
||||
|
||||
class TreeNode(ABC):
|
||||
"""Base class for tree nodes in the inspector."""
|
||||
|
||||
def __init__(self, label: str, parent: Optional['TreeNode'] = None):
|
||||
self.label = label
|
||||
self.parent = parent
|
||||
self.children: List['TreeNode'] = []
|
||||
self.is_expanded = False
|
||||
self.is_selected = False
|
||||
self.depth = 0 if parent is None else parent.depth + 1
|
||||
self.rect = pygame.Rect(0, 0, 0, 20) # Will be updated during layout
|
||||
|
||||
def add_child(self, child: 'TreeNode') -> None:
|
||||
"""Add a child node to this node."""
|
||||
child.parent = self
|
||||
child.depth = self.depth + 1
|
||||
self.children.append(child)
|
||||
|
||||
def remove_child(self, child: 'TreeNode') -> None:
|
||||
"""Remove a child node from this node."""
|
||||
if child in self.children:
|
||||
self.children.remove(child)
|
||||
child.parent = None
|
||||
|
||||
def toggle_expand(self) -> None:
|
||||
"""Toggle the expanded state of this node."""
|
||||
if self.children:
|
||||
self.is_expanded = not self.is_expanded
|
||||
|
||||
def expand(self) -> None:
|
||||
"""Expand this node."""
|
||||
if self.children:
|
||||
self.is_expanded = True
|
||||
|
||||
def collapse(self) -> None:
|
||||
"""Collapse this node."""
|
||||
self.is_expanded = False
|
||||
|
||||
def get_visible_children(self) -> List['TreeNode']:
|
||||
"""Get children that are currently visible."""
|
||||
if not self.is_expanded:
|
||||
return []
|
||||
return self.children
|
||||
|
||||
def get_all_visible_descendants(self) -> List['TreeNode']:
|
||||
"""Get all visible descendants of this node."""
|
||||
visible = []
|
||||
if self.is_expanded:
|
||||
for child in self.children:
|
||||
visible.append(child)
|
||||
visible.extend(child.get_all_visible_descendants())
|
||||
return visible
|
||||
|
||||
def is_leaf(self) -> bool:
|
||||
"""Check if this node has no children."""
|
||||
return len(self.children) == 0
|
||||
|
||||
@abstractmethod
|
||||
def get_display_text(self) -> str:
|
||||
"""Get the display text for this node."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def can_expand(self) -> bool:
|
||||
"""Check if this node can be expanded."""
|
||||
pass
|
||||
|
||||
def get_indent(self) -> int:
|
||||
"""Get the indentation width for this node."""
|
||||
return self.depth * 20
|
||||
|
||||
|
||||
class SimulationNode(TreeNode):
|
||||
"""Root node representing the entire simulation."""
|
||||
|
||||
def __init__(self, world):
|
||||
super().__init__("Simulation")
|
||||
self.world = world
|
||||
self.is_expanded = True # Always expanded
|
||||
|
||||
def get_display_text(self) -> str:
|
||||
return f"Simulation"
|
||||
|
||||
def can_expand(self) -> bool:
|
||||
return True
|
||||
|
||||
def update_entity_counts(self) -> None:
|
||||
"""Update entity type nodes with current world state."""
|
||||
entity_types = {}
|
||||
|
||||
# Count entities by type
|
||||
for entity in self.world.get_objects():
|
||||
entity_type = type(entity).__name__
|
||||
if entity_type not in entity_types:
|
||||
entity_types[entity_type] = []
|
||||
entity_types[entity_type].append(entity)
|
||||
|
||||
# Update children to match current entity types
|
||||
existing_types = {child.label: child for child in self.children}
|
||||
|
||||
# Remove types that no longer exist
|
||||
for type_name in existing_types:
|
||||
if type_name not in entity_types:
|
||||
self.remove_child(existing_types[type_name])
|
||||
|
||||
# Add or update type nodes
|
||||
for type_name, entities in entity_types.items():
|
||||
if type_name in existing_types:
|
||||
# Update existing node
|
||||
type_node = existing_types[type_name]
|
||||
type_node.update_entities(entities)
|
||||
else:
|
||||
# Create new type node
|
||||
type_node = EntityTypeNode(type_name, entities)
|
||||
self.add_child(type_node)
|
||||
|
||||
|
||||
class EntityTypeNode(TreeNode):
|
||||
"""Node representing a category of entities."""
|
||||
|
||||
def __init__(self, entity_type: str, entities: List[Any]):
|
||||
super().__init__(entity_type)
|
||||
self.entities = entities
|
||||
self._update_children()
|
||||
|
||||
def _update_children(self) -> None:
|
||||
"""Update child entity nodes to match current entities."""
|
||||
existing_ids = {child.entity_id: child for child in self.children}
|
||||
current_ids = {id(entity): entity for entity in self.entities}
|
||||
|
||||
# Remove entities that no longer exist
|
||||
for entity_id in existing_ids:
|
||||
if entity_id not in current_ids:
|
||||
self.remove_child(existing_ids[entity_id])
|
||||
|
||||
# Add or update entity nodes
|
||||
for entity_id, entity in current_ids.items():
|
||||
if entity_id in existing_ids:
|
||||
# Update existing node
|
||||
entity_node = existing_ids[entity_id]
|
||||
entity_node.entity = entity
|
||||
else:
|
||||
# Create new entity node
|
||||
entity_node = EntityNode(entity)
|
||||
self.add_child(entity_node)
|
||||
|
||||
def update_entities(self, entities: List[Any]) -> None:
|
||||
"""Update the entities for this type."""
|
||||
self.entities = entities
|
||||
self._update_children()
|
||||
|
||||
def get_display_text(self) -> str:
|
||||
count = len(self.entities)
|
||||
return f"{self.label} ({count})"
|
||||
|
||||
def can_expand(self) -> bool:
|
||||
return len(self.entities) > 0
|
||||
|
||||
|
||||
class EntityNode(TreeNode):
|
||||
"""Node representing an individual entity."""
|
||||
|
||||
def __init__(self, entity: Any):
|
||||
# Use entity ID as the initial label, will be updated in get_display_text
|
||||
super().__init__("", None)
|
||||
self.entity = entity
|
||||
self.entity_id = id(entity)
|
||||
self.is_leaf = True # Entity nodes can't have children
|
||||
|
||||
def get_display_text(self) -> str:
|
||||
# Start with just ID, designed for extensibility
|
||||
return f"Entity {self.entity_id}"
|
||||
|
||||
def can_expand(self) -> bool:
|
||||
return False
|
||||
|
||||
def is_leaf(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_entity_info(self) -> Dict[str, Any]:
|
||||
"""Get entity information for display purposes."""
|
||||
# Base implementation - can be extended later
|
||||
info = {
|
||||
'id': self.entity_id,
|
||||
'type': type(self.entity).__name__
|
||||
}
|
||||
|
||||
# Add position if available
|
||||
if hasattr(self.entity, 'position'):
|
||||
info['position'] = {
|
||||
'x': getattr(self.entity.position, 'x', 0),
|
||||
'y': getattr(self.entity.position, 'y', 0)
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
|
||||
class TreeSelectionManager:
|
||||
"""Manages selection state for tree nodes."""
|
||||
|
||||
def __init__(self):
|
||||
self.selected_nodes: List[TreeNode] = []
|
||||
self.last_selected_node: Optional[TreeNode] = None
|
||||
|
||||
def select_node(self, node: TreeNode, multi_select: bool = False) -> None:
|
||||
"""Select a node, optionally with multi-select."""
|
||||
if not multi_select:
|
||||
# Clear existing selection
|
||||
for selected_node in self.selected_nodes:
|
||||
selected_node.is_selected = False
|
||||
self.selected_nodes.clear()
|
||||
|
||||
if node.is_selected:
|
||||
# Deselect if already selected
|
||||
node.is_selected = False
|
||||
if node in self.selected_nodes:
|
||||
self.selected_nodes.remove(node)
|
||||
if node == self.last_selected_node:
|
||||
self.last_selected_node = None
|
||||
else:
|
||||
# Select the node
|
||||
node.is_selected = True
|
||||
self.selected_nodes.append(node)
|
||||
self.last_selected_node = node
|
||||
|
||||
def select_range(self, from_node: TreeNode, to_node: TreeNode, all_nodes: List[TreeNode]) -> None:
|
||||
"""Select a range of nodes between from_node and to_node."""
|
||||
if from_node not in all_nodes or to_node not in all_nodes:
|
||||
return
|
||||
|
||||
# Clear existing selection
|
||||
for selected_node in self.selected_nodes:
|
||||
selected_node.is_selected = False
|
||||
self.selected_nodes.clear()
|
||||
|
||||
# Find indices
|
||||
from_index = all_nodes.index(from_node)
|
||||
to_index = all_nodes.index(to_node)
|
||||
|
||||
# Select range
|
||||
start = min(from_index, to_index)
|
||||
end = max(from_index, to_index)
|
||||
|
||||
for i in range(start, end + 1):
|
||||
node = all_nodes[i]
|
||||
node.is_selected = True
|
||||
self.selected_nodes.append(node)
|
||||
|
||||
self.last_selected_node = to_node
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
"""Clear all selections."""
|
||||
for node in self.selected_nodes:
|
||||
node.is_selected = False
|
||||
self.selected_nodes.clear()
|
||||
self.last_selected_node = None
|
||||
|
||||
def get_selected_entities(self) -> List[Any]:
|
||||
"""Get the actual entities for selected entity nodes."""
|
||||
entities = []
|
||||
for node in self.selected_nodes:
|
||||
if isinstance(node, EntityNode):
|
||||
entities.append(node.entity)
|
||||
return entities
|
||||
597
ui/layout_manager.py
Normal file
597
ui/layout_manager.py
Normal file
@ -0,0 +1,597 @@
|
||||
"""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)
|
||||
313
ui/tree_widget.py
Normal file
313
ui/tree_widget.py
Normal file
@ -0,0 +1,313 @@
|
||||
"""
|
||||
Tree widget for displaying hierarchical inspector view.
|
||||
Handles rendering, interaction, and navigation of tree nodes.
|
||||
"""
|
||||
|
||||
import pygame
|
||||
import pygame_gui
|
||||
from pygame_gui.core import UIElement
|
||||
from typing import List, Optional, Tuple, Any
|
||||
from ui.inspector_tree import TreeNode, SimulationNode, TreeSelectionManager
|
||||
|
||||
|
||||
class TreeWidget(UIElement):
|
||||
"""Interactive tree widget for hierarchical entity inspection."""
|
||||
|
||||
def __init__(self, relative_rect: pygame.Rect, manager: pygame_gui.UIManager, world: Any):
|
||||
super().__init__(relative_rect, manager, container=None,
|
||||
starting_height=1, layer_thickness=1)
|
||||
|
||||
self.world = world
|
||||
self.font = pygame.font.Font(None, 16)
|
||||
self.small_font = pygame.font.Font(None, 12)
|
||||
|
||||
# Tree structure
|
||||
self.root_node = SimulationNode(world)
|
||||
self.visible_nodes: List[TreeNode] = []
|
||||
self.all_nodes: List[TreeNode] = []
|
||||
|
||||
# Selection management
|
||||
self.selection_manager = TreeSelectionManager()
|
||||
|
||||
# Visual properties
|
||||
self.node_height = 20
|
||||
self.expand_collapse_width = 20
|
||||
self.icon_size = 8
|
||||
self.text_color = (200, 200, 200)
|
||||
self.selected_color = (50, 100, 150)
|
||||
self.hover_color = (60, 60, 80)
|
||||
self.expand_icon_color = (150, 150, 150)
|
||||
|
||||
# Interaction state
|
||||
self.hovered_node: Optional[TreeNode] = None
|
||||
self.drag_start_node: Optional[TreeNode] = None
|
||||
self.is_dragging = False
|
||||
|
||||
# Scrolling
|
||||
self.scroll_offset = 0
|
||||
self.total_height = 0
|
||||
|
||||
# Initialize tree structure
|
||||
self._update_tree_structure()
|
||||
self._update_visible_nodes()
|
||||
|
||||
def _update_tree_structure(self) -> None:
|
||||
"""Update the tree structure based on current world state."""
|
||||
self.root_node.update_entity_counts()
|
||||
self.all_nodes = self._build_all_nodes_list(self.root_node)
|
||||
|
||||
def _build_all_nodes_list(self, node: TreeNode) -> List[TreeNode]:
|
||||
"""Build a flat list of all nodes in the tree."""
|
||||
nodes = [node]
|
||||
for child in node.children:
|
||||
nodes.extend(self._build_all_nodes_list(child))
|
||||
return nodes
|
||||
|
||||
def _update_visible_nodes(self) -> None:
|
||||
"""Update the list of visible nodes based on expanded state."""
|
||||
self.visible_nodes = [self.root_node]
|
||||
self.visible_nodes.extend(self.root_node.get_all_visible_descendants())
|
||||
self._update_node_layout()
|
||||
self.total_height = len(self.visible_nodes) * self.node_height
|
||||
|
||||
def _update_node_layout(self) -> None:
|
||||
"""Update the layout rectangles for all visible nodes."""
|
||||
y_offset = 0
|
||||
for node in self.visible_nodes:
|
||||
node.rect = pygame.Rect(
|
||||
0,
|
||||
y_offset - self.scroll_offset,
|
||||
self.rect.width,
|
||||
self.node_height
|
||||
)
|
||||
y_offset += self.node_height
|
||||
|
||||
def _get_node_at_position_local(self, local_pos: Tuple[int, int]) -> Optional[TreeNode]:
|
||||
"""Get the node at the given local position (already converted)."""
|
||||
for node in self.visible_nodes:
|
||||
if node.rect.collidepoint(local_pos):
|
||||
return node
|
||||
return None
|
||||
|
||||
def _get_node_at_position(self, pos: Tuple[int, int]) -> Optional[TreeNode]:
|
||||
"""Get the node at the given screen position."""
|
||||
# Convert screen coordinates to local tree widget coordinates
|
||||
local_pos = (pos[0] - self.rect.x, pos[1] - self.rect.y)
|
||||
return self._get_node_at_position_local(local_pos)
|
||||
|
||||
def _get_expand_collapse_rect(self, node: TreeNode) -> pygame.Rect:
|
||||
"""Get the rectangle for the expand/collapse icon."""
|
||||
indent = node.get_indent()
|
||||
return pygame.Rect(
|
||||
indent + 5,
|
||||
node.rect.y + (self.node_height - self.icon_size) // 2,
|
||||
self.icon_size,
|
||||
self.icon_size
|
||||
)
|
||||
|
||||
def _is_click_on_expand_collapse_local(self, node: TreeNode, local_pos: Tuple[int, int]) -> bool:
|
||||
"""Check if a click is on the expand/collapse icon (local coordinates)."""
|
||||
if not node.can_expand():
|
||||
return False
|
||||
|
||||
expand_rect = self._get_expand_collapse_rect(node)
|
||||
return expand_rect.collidepoint(local_pos)
|
||||
|
||||
def _is_click_on_expand_collapse(self, node: TreeNode, pos: Tuple[int, int]) -> bool:
|
||||
"""Check if a click is on the expand/collapse icon."""
|
||||
if not node.can_expand():
|
||||
return False
|
||||
|
||||
local_pos = (pos[0] - self.rect.x, pos[1] - self.rect.y)
|
||||
expand_rect = self._get_expand_collapse_rect(node)
|
||||
return expand_rect.collidepoint(local_pos)
|
||||
|
||||
def _render_node(self, surface: pygame.Surface, node: TreeNode) -> None:
|
||||
"""Render a single tree node."""
|
||||
if not node.rect.colliderect(surface.get_rect()):
|
||||
return
|
||||
|
||||
# Background
|
||||
if node.is_selected:
|
||||
pygame.draw.rect(surface, self.selected_color, node.rect)
|
||||
elif node == self.hovered_node:
|
||||
pygame.draw.rect(surface, self.hover_color, node.rect)
|
||||
|
||||
# Expand/collapse icon
|
||||
if node.can_expand():
|
||||
icon_rect = self._get_expand_collapse_rect(node)
|
||||
icon_color = self.expand_icon_color
|
||||
|
||||
# Draw + or - icon
|
||||
center_x = icon_rect.centerx
|
||||
center_y = icon_rect.centery
|
||||
size = 3
|
||||
|
||||
# Horizontal line
|
||||
pygame.draw.line(surface, icon_color,
|
||||
(center_x - size, center_y),
|
||||
(center_x + size, center_y), 1)
|
||||
|
||||
# Vertical line (only for collapsed nodes)
|
||||
if not node.is_expanded:
|
||||
pygame.draw.line(surface, icon_color,
|
||||
(center_x, center_y - size),
|
||||
(center_x, center_y + size), 1)
|
||||
|
||||
# Node text
|
||||
indent = node.get_indent()
|
||||
text_x = indent + (self.expand_collapse_width if node.can_expand() else 10)
|
||||
|
||||
text_surface = self.font.render(node.get_display_text(), True, self.text_color)
|
||||
text_rect = pygame.Rect(
|
||||
text_x,
|
||||
node.rect.y + (self.node_height - text_surface.get_height()) // 2,
|
||||
text_surface.get_width(),
|
||||
text_surface.get_height()
|
||||
)
|
||||
|
||||
# Clip text if it's too wide
|
||||
max_text_width = self.rect.width - text_x - 10
|
||||
if text_rect.width > max_text_width:
|
||||
# Truncate text
|
||||
text = node.get_display_text()
|
||||
while text and text_surface.get_width() > max_text_width - 20:
|
||||
text = text[:-1]
|
||||
text_surface = self.font.render(text + "...", True, self.text_color)
|
||||
|
||||
surface.blit(text_surface, text_rect)
|
||||
|
||||
def handle_event(self, event: pygame.event.Event) -> bool:
|
||||
"""Handle input events for the tree widget."""
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
if event.button == 1: # Left click
|
||||
# Event position is already converted to local coordinates by HUD
|
||||
node = self._get_node_at_position_local(event.pos)
|
||||
if node:
|
||||
# Check for expand/collapse click
|
||||
if self._is_click_on_expand_collapse_local(node, event.pos):
|
||||
node.toggle_expand()
|
||||
self._update_visible_nodes()
|
||||
else:
|
||||
# Selection click
|
||||
multi_select = pygame.key.get_pressed()[pygame.K_LSHIFT]
|
||||
self.selection_manager.select_node(node, multi_select)
|
||||
return True
|
||||
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
# Event position is already converted to local coordinates by HUD
|
||||
self.hovered_node = self._get_node_at_position_local(event.pos)
|
||||
return True
|
||||
|
||||
elif event.type == pygame.MOUSEWHEEL:
|
||||
# Handle scrolling
|
||||
if event.y > 0:
|
||||
self.scroll_offset = max(0, self.scroll_offset - self.node_height)
|
||||
else:
|
||||
max_scroll = max(0, self.total_height - self.rect.height)
|
||||
self.scroll_offset = min(max_scroll, self.scroll_offset + self.node_height)
|
||||
self._update_node_layout()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update(self, time_delta: float) -> None:
|
||||
"""Update the tree widget with smart performance optimizations."""
|
||||
# Smart update system: only update tree structure when necessary
|
||||
if self._should_update_tree():
|
||||
self._update_tree_structure()
|
||||
self._update_visible_nodes()
|
||||
|
||||
def _should_update_tree(self) -> bool:
|
||||
"""Determine if the tree structure needs updating."""
|
||||
if not hasattr(self, '_last_entity_count'):
|
||||
self._last_entity_count = len(self.world.get_objects())
|
||||
return True
|
||||
|
||||
current_count = len(self.world.get_objects())
|
||||
if current_count != self._last_entity_count:
|
||||
self._last_entity_count = current_count
|
||||
return True
|
||||
|
||||
# Also check if any previously selected entities have been removed
|
||||
if self.selection_manager.selected_nodes:
|
||||
for node in self.selection_manager.selected_nodes[:]:
|
||||
if hasattr(node, 'entity'):
|
||||
try:
|
||||
# Check if entity still exists in world
|
||||
if node.entity not in self.world.get_objects():
|
||||
return True
|
||||
except:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def draw(self, surface: pygame.Surface) -> None:
|
||||
"""Draw the tree widget."""
|
||||
# Create a clipping surface for the tree area
|
||||
tree_surface = pygame.Surface((self.rect.width, self.rect.height))
|
||||
tree_surface.fill((30, 30, 40)) # Background color
|
||||
|
||||
# Render visible nodes
|
||||
for node in self.visible_nodes:
|
||||
self._render_node(tree_surface, node)
|
||||
|
||||
# Blit to main surface at position (0,0) since this is already positioned
|
||||
surface.blit(tree_surface, (0, 0))
|
||||
|
||||
def expand_all(self) -> None:
|
||||
"""Expand all nodes in the tree."""
|
||||
for node in self.all_nodes:
|
||||
if node.can_expand():
|
||||
node.expand()
|
||||
self._update_visible_nodes()
|
||||
|
||||
def collapse_all(self) -> None:
|
||||
"""Collapse all nodes except the root."""
|
||||
for node in self.all_nodes[1:]: # Skip root node
|
||||
if node.can_expand():
|
||||
node.collapse()
|
||||
self._update_visible_nodes()
|
||||
|
||||
def get_selected_entities(self) -> List[Any]:
|
||||
"""Get the entities currently selected in the tree."""
|
||||
return self.selection_manager.get_selected_entities()
|
||||
|
||||
def select_entities(self, entities: List[Any]) -> None:
|
||||
"""Select specific entities in the tree."""
|
||||
self.selection_manager.clear_selection()
|
||||
|
||||
for entity in entities:
|
||||
# Find the entity node for this entity
|
||||
for node in self.all_nodes:
|
||||
if hasattr(node, 'entity') and node.entity == entity:
|
||||
self.selection_manager.select_node(node, multi_select=True)
|
||||
break
|
||||
|
||||
# Expand parent nodes to show selected entities
|
||||
for node in self.selection_manager.selected_nodes:
|
||||
parent = node.parent
|
||||
while parent:
|
||||
if parent.can_expand():
|
||||
parent.expand()
|
||||
parent = parent.parent
|
||||
|
||||
self._update_visible_nodes()
|
||||
|
||||
def scroll_to_node(self, node: TreeNode) -> None:
|
||||
"""Scroll the tree to show the given node."""
|
||||
if node not in self.visible_nodes:
|
||||
return
|
||||
|
||||
node_top = node.rect.y
|
||||
node_bottom = node.rect.y + node.rect.height
|
||||
|
||||
if node_top < 0:
|
||||
self.scroll_offset = max(0, self.scroll_offset + node_top)
|
||||
elif node_bottom > self.rect.height:
|
||||
self.scroll_offset = min(
|
||||
self.total_height - self.rect.height,
|
||||
self.scroll_offset + (node_bottom - self.rect.height)
|
||||
)
|
||||
|
||||
self._update_node_layout()
|
||||
Loading…
x
Reference in New Issue
Block a user