566 lines
25 KiB
Python
566 lines
25 KiB
Python
"""Settings UI renderer with sliders for the Village Simulation.
|
|
|
|
Includes settings for economy, religion, diplomacy, and oil.
|
|
"""
|
|
|
|
import pygame
|
|
from dataclasses import dataclass
|
|
from typing import Optional, Callable, Any
|
|
|
|
|
|
class Colors:
|
|
"""Color palette for settings UI."""
|
|
BG = (15, 17, 23)
|
|
PANEL_BG = (22, 26, 35)
|
|
PANEL_HEADER = (28, 33, 45)
|
|
PANEL_BORDER = (50, 60, 80)
|
|
TEXT_PRIMARY = (225, 228, 235)
|
|
TEXT_SECONDARY = (140, 150, 165)
|
|
TEXT_HIGHLIGHT = (100, 200, 255)
|
|
SLIDER_BG = (40, 45, 55)
|
|
SLIDER_FILL = (70, 130, 200)
|
|
SLIDER_HANDLE = (220, 220, 230)
|
|
BUTTON_BG = (50, 90, 150)
|
|
BUTTON_HOVER = (70, 110, 170)
|
|
BUTTON_TEXT = (255, 255, 255)
|
|
SUCCESS = (80, 180, 100)
|
|
WARNING = (200, 160, 80)
|
|
|
|
# Section colors
|
|
SECTION_ECONOMY = (100, 200, 255)
|
|
SECTION_WORLD = (100, 220, 150)
|
|
SECTION_RELIGION = (200, 150, 255)
|
|
SECTION_DIPLOMACY = (255, 180, 100)
|
|
SECTION_OIL = (180, 160, 100)
|
|
|
|
|
|
@dataclass
|
|
class SliderConfig:
|
|
"""Configuration for a slider widget."""
|
|
name: str
|
|
key: str
|
|
min_val: float
|
|
max_val: float
|
|
step: float = 1.0
|
|
is_int: bool = True
|
|
description: str = ""
|
|
section: str = "General"
|
|
|
|
|
|
# Organized slider configs by section
|
|
SLIDER_CONFIGS = [
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# WORLD SETTINGS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("World Width", "world.width", 15, 60, 5, True, "Grid width", "World"),
|
|
SliderConfig("World Height", "world.height", 15, 60, 5, True, "Grid height", "World"),
|
|
SliderConfig("Initial Agents", "world.initial_agents", 10, 200, 10, True, "Starting population", "World"),
|
|
SliderConfig("Day Steps", "world.day_steps", 5, 20, 1, True, "Steps per day", "World"),
|
|
SliderConfig("Starting Money", "world.starting_money", 50, 300, 25, True, "Initial coins", "World"),
|
|
SliderConfig("Inventory Slots", "world.inventory_slots", 6, 20, 2, True, "Max items", "World"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AGENT STATS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Max Energy", "agent_stats.max_energy", 30, 150, 10, True, "Energy capacity", "Stats"),
|
|
SliderConfig("Max Hunger", "agent_stats.max_hunger", 50, 200, 10, True, "Hunger capacity", "Stats"),
|
|
SliderConfig("Max Thirst", "agent_stats.max_thirst", 50, 200, 10, True, "Thirst capacity", "Stats"),
|
|
SliderConfig("Energy Decay", "agent_stats.energy_decay", 1, 5, 1, True, "Energy lost/turn", "Stats"),
|
|
SliderConfig("Hunger Decay", "agent_stats.hunger_decay", 1, 8, 1, True, "Hunger lost/turn", "Stats"),
|
|
SliderConfig("Thirst Decay", "agent_stats.thirst_decay", 1, 8, 1, True, "Thirst lost/turn", "Stats"),
|
|
SliderConfig("Critical %", "agent_stats.critical_threshold", 0.1, 0.4, 0.05, False, "Survival mode threshold", "Stats"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ACTIONS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Hunt Energy", "actions.hunt_energy", -15, -3, 1, True, "Energy cost", "Actions"),
|
|
SliderConfig("Gather Energy", "actions.gather_energy", -8, -1, 1, True, "Energy cost", "Actions"),
|
|
SliderConfig("Hunt Success %", "actions.hunt_success", 0.4, 1.0, 0.1, False, "Success chance", "Actions"),
|
|
SliderConfig("Sleep Restore", "actions.sleep_energy", 30, 80, 10, True, "Energy gained", "Actions"),
|
|
SliderConfig("Rest Restore", "actions.rest_energy", 5, 25, 5, True, "Energy gained", "Actions"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# RELIGION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Max Faith", "agent_stats.max_faith", 50, 150, 10, True, "Faith capacity", "Religion"),
|
|
SliderConfig("Faith Decay", "agent_stats.faith_decay", 0, 5, 1, True, "Faith lost/turn", "Religion"),
|
|
SliderConfig("Pray Faith Gain", "actions.pray_faith_gain", 10, 50, 5, True, "Faith from prayer", "Religion"),
|
|
SliderConfig("Convert Chance", "actions.preach_convert_chance", 0.05, 0.4, 0.05, False, "Conversion rate", "Religion"),
|
|
SliderConfig("Zealot Threshold", "religion.zealot_threshold", 0.6, 0.95, 0.05, False, "Zealot faith %", "Religion"),
|
|
SliderConfig("Same Religion Bonus", "religion.same_religion_bonus", 0.0, 0.3, 0.05, False, "Trade bonus", "Religion"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# DIPLOMACY
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Num Factions", "diplomacy.num_factions", 2, 8, 1, True, "Active factions", "Diplomacy"),
|
|
SliderConfig("Starting Relations", "diplomacy.starting_relations", 30, 70, 5, True, "Initial relation", "Diplomacy"),
|
|
SliderConfig("Alliance Threshold", "diplomacy.alliance_threshold", 60, 90, 5, True, "For alliance", "Diplomacy"),
|
|
SliderConfig("War Threshold", "diplomacy.war_threshold", 10, 40, 5, True, "For war", "Diplomacy"),
|
|
SliderConfig("Relation Decay", "diplomacy.relation_decay", 0, 5, 1, True, "Decay per turn", "Diplomacy"),
|
|
SliderConfig("War Exhaustion", "diplomacy.war_exhaustion_rate", 1, 10, 1, True, "Exhaustion/turn", "Diplomacy"),
|
|
SliderConfig("Peace Duration", "diplomacy.peace_treaty_duration", 10, 50, 5, True, "Treaty turns", "Diplomacy"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# OIL & RESOURCES
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Oil Fields", "world.oil_fields_count", 1, 10, 1, True, "Number of fields", "Oil"),
|
|
SliderConfig("Drill Energy", "actions.drill_oil_energy", -20, -5, 1, True, "Drill cost", "Oil"),
|
|
SliderConfig("Drill Success %", "actions.drill_oil_success", 0.3, 1.0, 0.1, False, "Success chance", "Oil"),
|
|
SliderConfig("Oil Base Price", "economy.oil_base_price", 10, 50, 5, True, "Market price", "Oil"),
|
|
SliderConfig("Fuel Base Price", "economy.fuel_base_price", 20, 80, 5, True, "Market price", "Oil"),
|
|
SliderConfig("Fuel Heat", "resources.fuel_heat", 20, 60, 5, True, "Heat provided", "Oil"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MARKET
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Discount Turns", "market.turns_before_discount", 5, 30, 5, True, "Before price drop", "Market"),
|
|
SliderConfig("Discount Rate %", "market.discount_rate", 0.05, 0.25, 0.05, False, "Per period", "Market"),
|
|
SliderConfig("Max Markup", "economy.max_price_markup", 1.5, 4.0, 0.5, False, "Price ceiling", "Market"),
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SIMULATION
|
|
# ═══════════════════════════════════════════════════════════════
|
|
SliderConfig("Auto Step (s)", "auto_step_interval", 0.1, 2.0, 0.1, False, "Seconds/step", "Simulation"),
|
|
]
|
|
|
|
# Section order and colors
|
|
SECTION_ORDER = ["World", "Stats", "Actions", "Religion", "Diplomacy", "Oil", "Market", "Simulation"]
|
|
SECTION_COLORS = {
|
|
"World": Colors.SECTION_WORLD,
|
|
"Stats": Colors.SECTION_ECONOMY,
|
|
"Actions": Colors.SECTION_ECONOMY,
|
|
"Religion": Colors.SECTION_RELIGION,
|
|
"Diplomacy": Colors.SECTION_DIPLOMACY,
|
|
"Oil": Colors.SECTION_OIL,
|
|
"Market": Colors.SECTION_ECONOMY,
|
|
"Simulation": Colors.TEXT_SECONDARY,
|
|
}
|
|
|
|
|
|
class Slider:
|
|
"""A slider widget for adjusting numeric values."""
|
|
|
|
def __init__(
|
|
self,
|
|
rect: pygame.Rect,
|
|
config: SliderConfig,
|
|
font: pygame.font.Font,
|
|
small_font: pygame.font.Font,
|
|
):
|
|
self.rect = rect
|
|
self.config = config
|
|
self.font = font
|
|
self.small_font = small_font
|
|
self.value = config.min_val
|
|
self.dragging = False
|
|
self.hovered = False
|
|
|
|
def set_value(self, value: float) -> None:
|
|
"""Set slider value."""
|
|
self.value = max(self.config.min_val, min(self.config.max_val, value))
|
|
if self.config.is_int:
|
|
self.value = int(round(self.value))
|
|
|
|
def get_value(self) -> Any:
|
|
"""Get current value."""
|
|
return int(self.value) if self.config.is_int else round(self.value, 2)
|
|
|
|
def handle_event(self, event: pygame.event.Event) -> bool:
|
|
"""Handle events."""
|
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
if self._slider_area().collidepoint(event.pos):
|
|
self.dragging = True
|
|
return self._update_from_mouse(event.pos[0])
|
|
|
|
elif event.type == pygame.MOUSEBUTTONUP:
|
|
self.dragging = False
|
|
|
|
elif event.type == pygame.MOUSEMOTION:
|
|
self.hovered = self.rect.collidepoint(event.pos)
|
|
if self.dragging:
|
|
return self._update_from_mouse(event.pos[0])
|
|
|
|
return False
|
|
|
|
def _slider_area(self) -> pygame.Rect:
|
|
"""Get slider track area."""
|
|
return pygame.Rect(
|
|
self.rect.x + 130,
|
|
self.rect.y + 12,
|
|
self.rect.width - 200,
|
|
16,
|
|
)
|
|
|
|
def _update_from_mouse(self, mouse_x: int) -> bool:
|
|
"""Update value from mouse."""
|
|
slider_area = self._slider_area()
|
|
rel_x = mouse_x - slider_area.x
|
|
ratio = max(0, min(1, rel_x / slider_area.width))
|
|
|
|
range_val = self.config.max_val - self.config.min_val
|
|
new_value = self.config.min_val + ratio * range_val
|
|
|
|
if self.config.step > 0:
|
|
new_value = round(new_value / self.config.step) * self.config.step
|
|
|
|
old_value = self.value
|
|
self.set_value(new_value)
|
|
return abs(old_value - self.value) > 0.001
|
|
|
|
def draw(self, screen: pygame.Surface, section_color: tuple) -> None:
|
|
"""Draw the slider."""
|
|
# Hover highlight
|
|
if self.hovered:
|
|
pygame.draw.rect(screen, (35, 40, 50), self.rect, border_radius=4)
|
|
|
|
# Label
|
|
label = self.small_font.render(self.config.name, True, Colors.TEXT_PRIMARY)
|
|
screen.blit(label, (self.rect.x + 8, self.rect.y + 6))
|
|
|
|
# Slider track
|
|
slider_area = self._slider_area()
|
|
pygame.draw.rect(screen, Colors.SLIDER_BG, slider_area, border_radius=4)
|
|
|
|
# Slider fill
|
|
ratio = (self.value - self.config.min_val) / (self.config.max_val - self.config.min_val)
|
|
fill_width = int(ratio * slider_area.width)
|
|
fill_rect = pygame.Rect(slider_area.x, slider_area.y, fill_width, slider_area.height)
|
|
pygame.draw.rect(screen, section_color, fill_rect, border_radius=4)
|
|
|
|
# Handle
|
|
handle_x = slider_area.x + fill_width
|
|
handle_rect = pygame.Rect(handle_x - 5, slider_area.y - 2, 10, slider_area.height + 4)
|
|
pygame.draw.rect(screen, Colors.SLIDER_HANDLE, handle_rect, border_radius=3)
|
|
|
|
# Value display
|
|
value_str = str(self.get_value())
|
|
value_text = self.small_font.render(value_str, True, Colors.TEXT_HIGHLIGHT)
|
|
screen.blit(value_text, (self.rect.right - 55, self.rect.y + 6))
|
|
|
|
# Description on hover
|
|
if self.hovered and self.config.description:
|
|
desc = self.small_font.render(self.config.description, True, Colors.TEXT_SECONDARY)
|
|
screen.blit(desc, (self.rect.x + 8, self.rect.y + 24))
|
|
|
|
|
|
class Button:
|
|
"""Button widget."""
|
|
|
|
def __init__(
|
|
self,
|
|
rect: pygame.Rect,
|
|
text: str,
|
|
font: pygame.font.Font,
|
|
callback: Optional[Callable] = None,
|
|
color: tuple = Colors.BUTTON_BG,
|
|
):
|
|
self.rect = rect
|
|
self.text = text
|
|
self.font = font
|
|
self.callback = callback
|
|
self.color = color
|
|
self.hovered = False
|
|
|
|
def handle_event(self, event: pygame.event.Event) -> bool:
|
|
"""Handle events."""
|
|
if event.type == pygame.MOUSEMOTION:
|
|
self.hovered = self.rect.collidepoint(event.pos)
|
|
|
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
if self.rect.collidepoint(event.pos):
|
|
if self.callback:
|
|
self.callback()
|
|
return True
|
|
|
|
return False
|
|
|
|
def draw(self, screen: pygame.Surface) -> None:
|
|
"""Draw button."""
|
|
color = Colors.BUTTON_HOVER if self.hovered else self.color
|
|
pygame.draw.rect(screen, color, self.rect, border_radius=6)
|
|
pygame.draw.rect(screen, Colors.PANEL_BORDER, self.rect, 1, border_radius=6)
|
|
|
|
text = self.font.render(self.text, True, Colors.BUTTON_TEXT)
|
|
text_rect = text.get_rect(center=self.rect.center)
|
|
screen.blit(text, text_rect)
|
|
|
|
|
|
class SettingsRenderer:
|
|
"""Settings panel with organized sections and sliders."""
|
|
|
|
def __init__(self, screen: pygame.Surface):
|
|
self.screen = screen
|
|
self.font = pygame.font.Font(None, 22)
|
|
self.small_font = pygame.font.Font(None, 16)
|
|
self.title_font = pygame.font.Font(None, 28)
|
|
self.section_font = pygame.font.Font(None, 20)
|
|
|
|
self.visible = False
|
|
self.scroll_offset = 0
|
|
self.max_scroll = 0
|
|
self.current_section = 0
|
|
|
|
self.sliders: list[Slider] = []
|
|
self.buttons: list[Button] = []
|
|
self.section_tabs: list[pygame.Rect] = []
|
|
self.config_data: dict = {}
|
|
|
|
self._create_widgets()
|
|
self.status_message = ""
|
|
self.status_color = Colors.TEXT_SECONDARY
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""Create widgets."""
|
|
screen_w, screen_h = self.screen.get_size()
|
|
|
|
# Panel dimensions - wider for better readability
|
|
panel_width = min(600, screen_w - 100)
|
|
panel_height = screen_h - 80
|
|
panel_x = (screen_w - panel_width) // 2
|
|
panel_y = 40
|
|
|
|
self.panel_rect = pygame.Rect(panel_x, panel_y, panel_width, panel_height)
|
|
|
|
# Tab bar for sections
|
|
tab_height = 30
|
|
self.tab_rect = pygame.Rect(panel_x, panel_y, panel_width, tab_height)
|
|
|
|
# Content area
|
|
content_start_y = panel_y + tab_height + 10
|
|
slider_height = 38
|
|
|
|
# Group sliders by section
|
|
self.sliders_by_section: dict[str, list[Slider]] = {s: [] for s in SECTION_ORDER}
|
|
|
|
slider_width = panel_width - 40
|
|
|
|
for config in SLIDER_CONFIGS:
|
|
rect = pygame.Rect(panel_x + 20, 0, slider_width, slider_height)
|
|
slider = Slider(rect, config, self.font, self.small_font)
|
|
self.sliders.append(slider)
|
|
self.sliders_by_section[config.section].append(slider)
|
|
|
|
# Calculate positions for current section
|
|
self._layout_current_section()
|
|
|
|
# Buttons at bottom
|
|
button_y = panel_y + panel_height - 50
|
|
button_width = 120
|
|
button_height = 35
|
|
button_spacing = 15
|
|
|
|
buttons_data = [
|
|
("Apply & Restart", self._apply_config, Colors.SUCCESS),
|
|
("Reset Defaults", self._reset_config, Colors.WARNING),
|
|
("Close", self.toggle, Colors.PANEL_BORDER),
|
|
]
|
|
|
|
total_w = len(buttons_data) * button_width + (len(buttons_data) - 1) * button_spacing
|
|
start_x = panel_x + (panel_width - total_w) // 2
|
|
|
|
for i, (text, callback, color) in enumerate(buttons_data):
|
|
rect = pygame.Rect(
|
|
start_x + i * (button_width + button_spacing),
|
|
button_y,
|
|
button_width,
|
|
button_height,
|
|
)
|
|
self.buttons.append(Button(rect, text, self.small_font, callback, color))
|
|
|
|
def _layout_current_section(self) -> None:
|
|
"""Layout sliders for current section."""
|
|
section = SECTION_ORDER[self.current_section]
|
|
sliders = self.sliders_by_section[section]
|
|
|
|
content_y = self.panel_rect.y + 50
|
|
slider_height = 38
|
|
|
|
for i, slider in enumerate(sliders):
|
|
slider.rect.y = content_y + i * slider_height - self.scroll_offset
|
|
|
|
# Calculate max scroll
|
|
total_height = len(sliders) * slider_height
|
|
visible_height = self.panel_rect.height - 120
|
|
self.max_scroll = max(0, total_height - visible_height)
|
|
|
|
def toggle(self) -> None:
|
|
"""Toggle visibility."""
|
|
self.visible = not self.visible
|
|
if self.visible:
|
|
self.scroll_offset = 0
|
|
self._layout_current_section()
|
|
|
|
def set_config(self, config_data: dict) -> None:
|
|
"""Set slider values from config."""
|
|
self.config_data = config_data
|
|
|
|
for slider in self.sliders:
|
|
value = self._get_nested_value(config_data, slider.config.key)
|
|
if value is not None:
|
|
slider.set_value(value)
|
|
|
|
def get_config(self) -> dict:
|
|
"""Get config from sliders."""
|
|
result = {}
|
|
for slider in self.sliders:
|
|
self._set_nested_value(result, slider.config.key, slider.get_value())
|
|
return result
|
|
|
|
def _get_nested_value(self, data: dict, key: str) -> Any:
|
|
"""Get nested dict value."""
|
|
parts = key.split(".")
|
|
current = data
|
|
for part in parts:
|
|
if isinstance(current, dict) and part in current:
|
|
current = current[part]
|
|
else:
|
|
return None
|
|
return current
|
|
|
|
def _set_nested_value(self, data: dict, key: str, value: Any) -> None:
|
|
"""Set nested dict value."""
|
|
parts = key.split(".")
|
|
current = data
|
|
for part in parts[:-1]:
|
|
if part not in current:
|
|
current[part] = {}
|
|
current = current[part]
|
|
current[parts[-1]] = value
|
|
|
|
def _apply_config(self) -> None:
|
|
"""Apply config callback."""
|
|
self.status_message = "Config applied - restart to see changes"
|
|
self.status_color = Colors.SUCCESS
|
|
|
|
def _reset_config(self) -> None:
|
|
"""Reset config callback."""
|
|
self.status_message = "Config reset to defaults"
|
|
self.status_color = Colors.WARNING
|
|
|
|
def handle_event(self, event: pygame.event.Event) -> bool:
|
|
"""Handle events."""
|
|
if not self.visible:
|
|
return False
|
|
|
|
# Tab clicks
|
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
if self.tab_rect.collidepoint(event.pos):
|
|
tab_width = self.panel_rect.width // len(SECTION_ORDER)
|
|
rel_x = event.pos[0] - self.tab_rect.x
|
|
tab_idx = rel_x // tab_width
|
|
if 0 <= tab_idx < len(SECTION_ORDER) and tab_idx != self.current_section:
|
|
self.current_section = tab_idx
|
|
self.scroll_offset = 0
|
|
self._layout_current_section()
|
|
return True
|
|
|
|
# Scrolling
|
|
if event.type == pygame.MOUSEWHEEL:
|
|
self.scroll_offset -= event.y * 30
|
|
self.scroll_offset = max(0, min(self.max_scroll, self.scroll_offset))
|
|
self._layout_current_section()
|
|
return True
|
|
|
|
# Sliders for current section
|
|
section = SECTION_ORDER[self.current_section]
|
|
for slider in self.sliders_by_section[section]:
|
|
adjusted_rect = slider.rect.copy()
|
|
|
|
if slider.handle_event(event):
|
|
return True
|
|
|
|
# Buttons
|
|
for button in self.buttons:
|
|
if button.handle_event(event):
|
|
return True
|
|
|
|
# Consume clicks
|
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
|
return True
|
|
|
|
return False
|
|
|
|
def draw(self) -> None:
|
|
"""Draw settings panel."""
|
|
if not self.visible:
|
|
return
|
|
|
|
# Dim background
|
|
overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA)
|
|
overlay.fill((0, 0, 0, 220))
|
|
self.screen.blit(overlay, (0, 0))
|
|
|
|
# Panel
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BG, self.panel_rect, border_radius=10)
|
|
pygame.draw.rect(self.screen, Colors.PANEL_BORDER, self.panel_rect, 2, border_radius=10)
|
|
|
|
# Title
|
|
title = self.title_font.render("Simulation Settings", True, Colors.TEXT_PRIMARY)
|
|
title_rect = title.get_rect(centerx=self.panel_rect.centerx, y=self.panel_rect.y + 8)
|
|
self.screen.blit(title, title_rect)
|
|
|
|
# Section tabs
|
|
self._draw_section_tabs()
|
|
|
|
# Clipping for sliders
|
|
clip_rect = pygame.Rect(
|
|
self.panel_rect.x + 10,
|
|
self.panel_rect.y + 45,
|
|
self.panel_rect.width - 20,
|
|
self.panel_rect.height - 110,
|
|
)
|
|
|
|
# Draw sliders for current section
|
|
section = SECTION_ORDER[self.current_section]
|
|
section_color = SECTION_COLORS.get(section, Colors.TEXT_SECONDARY)
|
|
|
|
for slider in self.sliders_by_section[section]:
|
|
if clip_rect.colliderect(slider.rect):
|
|
slider.draw(self.screen, section_color)
|
|
|
|
# Scroll indicator
|
|
if self.max_scroll > 0:
|
|
scroll_ratio = self.scroll_offset / self.max_scroll
|
|
bar_height = max(30, int((clip_rect.height / (clip_rect.height + self.max_scroll)) * clip_rect.height))
|
|
bar_y = clip_rect.y + int(scroll_ratio * (clip_rect.height - bar_height))
|
|
bar_rect = pygame.Rect(self.panel_rect.right - 12, bar_y, 5, bar_height)
|
|
pygame.draw.rect(self.screen, Colors.SLIDER_FILL, bar_rect, border_radius=2)
|
|
|
|
# Buttons
|
|
for button in self.buttons:
|
|
button.draw(self.screen)
|
|
|
|
# Status message
|
|
if self.status_message:
|
|
status = self.small_font.render(self.status_message, True, self.status_color)
|
|
status_rect = status.get_rect(
|
|
centerx=self.panel_rect.centerx,
|
|
y=self.panel_rect.bottom - 80
|
|
)
|
|
self.screen.blit(status, status_rect)
|
|
|
|
def _draw_section_tabs(self) -> None:
|
|
"""Draw section tabs."""
|
|
tab_width = self.panel_rect.width // len(SECTION_ORDER)
|
|
tab_y = self.panel_rect.y + 32
|
|
tab_height = 20
|
|
|
|
for i, section in enumerate(SECTION_ORDER):
|
|
tab_x = self.panel_rect.x + i * tab_width
|
|
tab_rect = pygame.Rect(tab_x, tab_y, tab_width, tab_height)
|
|
|
|
color = SECTION_COLORS.get(section, Colors.TEXT_SECONDARY)
|
|
|
|
if i == self.current_section:
|
|
pygame.draw.rect(self.screen, color, tab_rect, border_radius=3)
|
|
text_color = Colors.BG
|
|
else:
|
|
pygame.draw.rect(self.screen, (40, 45, 55), tab_rect, border_radius=3)
|
|
text_color = color
|
|
|
|
# Section name (abbreviated)
|
|
name = section[:5] if len(section) > 5 else section
|
|
text = self.small_font.render(name, True, text_color)
|
|
text_rect = text.get_rect(center=tab_rect.center)
|
|
self.screen.blit(text, text_rect)
|