257 lines
11 KiB
Python
257 lines
11 KiB
Python
import pygame
|
||
|
||
class PyGUI:
|
||
_surface = None
|
||
_font = None
|
||
_events = []
|
||
_window_stack = []
|
||
|
||
# UI layout constants
|
||
TITLE_BAR_HEIGHT = 30
|
||
PADDING = 10
|
||
SPACING = 5
|
||
BOTTOM_PADDING = 10
|
||
MIN_WINDOW_WIDTH = 220
|
||
|
||
# Colors and style
|
||
WINDOW_BG_COLOR = (60, 60, 60)
|
||
TITLE_BAR_COLOR = (30, 30, 30)
|
||
BORDER_COLOR = (20, 20, 20)
|
||
BUTTON_COLOR = (100, 100, 100)
|
||
BUTTON_HOVER_COLOR = (130, 130, 130)
|
||
SLIDER_TRACK_COLOR = (100, 100, 100)
|
||
SLIDER_KNOB_COLOR = (150, 150, 150)
|
||
WIDGET_TEXT_COLOR = (255, 255, 255)
|
||
CHECKBOX_BG_COLOR = (80, 80, 80)
|
||
CHECKBOX_CHECK_COLOR = (200, 200, 200)
|
||
|
||
@staticmethod
|
||
def init(surface, font_name='Arial', font_size=18):
|
||
"""Initialize PyGUI with the main Pygame surface and font."""
|
||
PyGUI._surface = surface
|
||
PyGUI._font = pygame.font.SysFont(font_name, font_size)
|
||
|
||
@staticmethod
|
||
def Begin(title, x, y):
|
||
"""
|
||
Begin a new draggable, auto–sizing window.
|
||
A new window context is created; widget calls will record
|
||
their draw commands and update the context layout.
|
||
"""
|
||
context = {
|
||
'title': title,
|
||
'x': x,
|
||
'y': y,
|
||
'width': PyGUI.MIN_WINDOW_WIDTH,
|
||
'content_height': 0, # the vertical extent of all widgets
|
||
'commands': [], # list of widget draw command lambdas
|
||
'drag': False,
|
||
'drag_offset': (0, 0),
|
||
'local_y': 0 # current Y offset (relative to content area)
|
||
}
|
||
PyGUI._window_stack.append(context)
|
||
|
||
@staticmethod
|
||
def End():
|
||
"""
|
||
Ends the current window, calculates final dimensions,
|
||
draws the window background, title bar and border, then blits the window.
|
||
"""
|
||
if not PyGUI._window_stack:
|
||
return
|
||
context = PyGUI._window_stack.pop()
|
||
final_height = PyGUI.TITLE_BAR_HEIGHT + context['content_height'] + PyGUI.BOTTOM_PADDING
|
||
|
||
# Create an offscreen surface for the entire window with alpha support.
|
||
window_surf = pygame.Surface((context['width'], final_height), pygame.SRCALPHA)
|
||
# Draw the window background with rounded corners.
|
||
pygame.draw.rect(window_surf, PyGUI.WINDOW_BG_COLOR,
|
||
(0, 0, context['width'], final_height), border_radius=8)
|
||
# Execute widget drawing commands onto window_surf.
|
||
for cmd in context['commands']:
|
||
cmd(window_surf)
|
||
# Draw the title bar on top.
|
||
title_bar_rect = pygame.Rect(0, 0, context['width'], PyGUI.TITLE_BAR_HEIGHT)
|
||
pygame.draw.rect(window_surf, PyGUI.TITLE_BAR_COLOR, title_bar_rect, border_radius=8)
|
||
# Render the window title.
|
||
title_surf = PyGUI._font.render(context['title'], True, PyGUI.WIDGET_TEXT_COLOR)
|
||
window_surf.blit(title_surf, (PyGUI.PADDING, (PyGUI.TITLE_BAR_HEIGHT - title_surf.get_height()) // 2))
|
||
# Draw a border around the entire window.
|
||
pygame.draw.rect(window_surf, PyGUI.BORDER_COLOR,
|
||
(0, 0, context['width'], final_height), 2, border_radius=8)
|
||
# Blit the finished window onto the main surface.
|
||
PyGUI._surface.blit(window_surf, (context['x'], context['y']))
|
||
|
||
@staticmethod
|
||
def _current_window():
|
||
if not PyGUI._window_stack:
|
||
return None
|
||
return PyGUI._window_stack[-1]
|
||
|
||
@staticmethod
|
||
def Update(events):
|
||
"""Update the internal event cache and process dragging for all windows."""
|
||
PyGUI._events = events
|
||
for context in PyGUI._window_stack:
|
||
PyGUI._process_dragging(context)
|
||
|
||
@staticmethod
|
||
def _process_dragging(context):
|
||
"""If the mouse clicks the title bar, allow dragging to update window position."""
|
||
mouse_pos = pygame.mouse.get_pos()
|
||
mouse_buttons = pygame.mouse.get_pressed()
|
||
title_rect = pygame.Rect(context['x'], context['y'], context['width'], PyGUI.TITLE_BAR_HEIGHT)
|
||
for event in PyGUI._events:
|
||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||
if title_rect.collidepoint(mouse_pos):
|
||
context['drag'] = True
|
||
context['drag_offset'] = (mouse_pos[0] - context['x'],
|
||
mouse_pos[1] - context['y'])
|
||
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
||
context['drag'] = False
|
||
if context['drag'] and mouse_buttons[0]:
|
||
context['x'] = mouse_pos[0] - context['drag_offset'][0]
|
||
context['y'] = mouse_pos[1] - context['drag_offset'][1]
|
||
|
||
@staticmethod
|
||
def Button(label, width=100, height=30):
|
||
"""
|
||
Create a button widget; returns True if the button is clicked.
|
||
The interactive test is done immediately and the draw command is recorded.
|
||
"""
|
||
context = PyGUI._current_window()
|
||
if context is None:
|
||
return False
|
||
|
||
# Compute local widget position inside the window's content area.
|
||
local_x = PyGUI.PADDING
|
||
local_y = context['local_y'] + PyGUI.PADDING
|
||
# Calculate absolute position for interaction.
|
||
abs_x = context['x'] + local_x
|
||
abs_y = context['y'] + PyGUI.TITLE_BAR_HEIGHT + local_y
|
||
widget_rect = pygame.Rect(abs_x, abs_y, width, height)
|
||
|
||
clicked = False
|
||
hovered = widget_rect.collidepoint(pygame.mouse.get_pos())
|
||
for event in PyGUI._events:
|
||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and hovered:
|
||
clicked = True
|
||
|
||
color = PyGUI.BUTTON_HOVER_COLOR if hovered else PyGUI.BUTTON_COLOR
|
||
|
||
# Record the draw command (drawing relative to the window surface).
|
||
def draw_command(surf):
|
||
btn_rect = pygame.Rect(local_x, local_y, width, height)
|
||
pygame.draw.rect(surf, color, btn_rect, border_radius=4)
|
||
text_surf = PyGUI._font.render(label, True, PyGUI.WIDGET_TEXT_COLOR)
|
||
text_rect = text_surf.get_rect(center=btn_rect.center)
|
||
surf.blit(text_surf, text_rect)
|
||
|
||
context['commands'].append(draw_command)
|
||
context['local_y'] += height + PyGUI.SPACING
|
||
context['content_height'] = max(context['content_height'],
|
||
context['local_y'] + PyGUI.PADDING)
|
||
needed_width = local_x + width + PyGUI.PADDING
|
||
if needed_width > context['width']:
|
||
context['width'] = needed_width
|
||
|
||
return clicked
|
||
|
||
@staticmethod
|
||
def Slider(label, value, min_val, max_val, width=150, height=20):
|
||
"""
|
||
Create a slider widget; returns the updated value.
|
||
A label is drawn above the slider track.
|
||
"""
|
||
context = PyGUI._current_window()
|
||
if context is None:
|
||
return value
|
||
|
||
local_x = PyGUI.PADDING
|
||
local_y = context['local_y'] + PyGUI.PADDING
|
||
abs_x = context['x'] + local_x
|
||
abs_y = context['y'] + PyGUI.TITLE_BAR_HEIGHT + local_y
|
||
|
||
# Render label (with current value) and get its height.
|
||
label_surf = PyGUI._font.render(f"{label}: {value:.2f}", True, PyGUI.WIDGET_TEXT_COLOR)
|
||
label_height = label_surf.get_height()
|
||
|
||
# Define slider track's absolute rectangle.
|
||
track_rect = pygame.Rect(abs_x, abs_y + label_height + 2, width, height)
|
||
new_value = value
|
||
if track_rect.collidepoint(pygame.mouse.get_pos()) and pygame.mouse.get_pressed()[0]:
|
||
mouse_x = pygame.mouse.get_pos()[0]
|
||
relative = (mouse_x - track_rect.x) / track_rect.width
|
||
relative = max(0.0, min(1.0, relative))
|
||
new_value = min_val + relative * (max_val - min_val)
|
||
|
||
relative = (new_value - min_val) / (max_val - min_val)
|
||
|
||
def draw_command(surf):
|
||
# Draw the label.
|
||
surf.blit(label_surf, (local_x, local_y))
|
||
# Draw the slider track.
|
||
local_track_rect = pygame.Rect(local_x, local_y + label_height + 2, width, height)
|
||
pygame.draw.rect(surf, PyGUI.SLIDER_TRACK_COLOR, local_track_rect, border_radius=4)
|
||
# Draw the knob.
|
||
knob_rect = pygame.Rect(local_track_rect.x + int(relative * local_track_rect.width) - 5,
|
||
local_track_rect.y - 2, 10, local_track_rect.height + 4)
|
||
pygame.draw.rect(surf, PyGUI.SLIDER_KNOB_COLOR, knob_rect, border_radius=4)
|
||
|
||
context['commands'].append(draw_command)
|
||
total_height = label_height + 2 + height + PyGUI.SPACING
|
||
context['local_y'] += total_height
|
||
context['content_height'] = max(context['content_height'],
|
||
context['local_y'] + PyGUI.PADDING)
|
||
if local_x + width + PyGUI.PADDING > context['width']:
|
||
context['width'] = local_x + width + PyGUI.PADDING
|
||
|
||
return new_value
|
||
|
||
@staticmethod
|
||
def Checkbox(label, value):
|
||
"""
|
||
Create a checkbox widget; returns the toggled boolean value.
|
||
"""
|
||
context = PyGUI._current_window()
|
||
if context is None:
|
||
return value
|
||
|
||
local_x = PyGUI.PADDING
|
||
local_y = context['local_y'] + PyGUI.PADDING
|
||
abs_x = context['x'] + local_x
|
||
abs_y = context['y'] + PyGUI.TITLE_BAR_HEIGHT + local_y
|
||
|
||
box_size = 20
|
||
box_rect = pygame.Rect(abs_x, abs_y, box_size, box_size)
|
||
new_value = value
|
||
hovered = box_rect.collidepoint(pygame.mouse.get_pos())
|
||
for event in PyGUI._events:
|
||
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and hovered:
|
||
new_value = not value
|
||
|
||
def draw_command(surf):
|
||
local_box_rect = pygame.Rect(local_x, local_y, box_size, box_size)
|
||
pygame.draw.rect(surf, PyGUI.CHECKBOX_BG_COLOR, local_box_rect, border_radius=3)
|
||
if new_value:
|
||
pygame.draw.line(surf, PyGUI.CHECKBOX_CHECK_COLOR,
|
||
(local_box_rect.left, local_box_rect.top),
|
||
(local_box_rect.right, local_box_rect.bottom), 2)
|
||
pygame.draw.line(surf, PyGUI.CHECKBOX_CHECK_COLOR,
|
||
(local_box_rect.left, local_box_rect.bottom),
|
||
(local_box_rect.right, local_box_rect.top), 2)
|
||
text_surf = PyGUI._font.render(label, True, PyGUI.WIDGET_TEXT_COLOR)
|
||
surf.blit(text_surf, (local_box_rect.right + 5, local_box_rect.top))
|
||
|
||
context['commands'].append(draw_command)
|
||
text_surf = PyGUI._font.render(label, True, PyGUI.WIDGET_TEXT_COLOR)
|
||
line_height = max(box_size, text_surf.get_height())
|
||
context['local_y'] += line_height + PyGUI.SPACING
|
||
context['content_height'] = max(context['content_height'],
|
||
context['local_y'] + PyGUI.PADDING)
|
||
needed_width = local_x + box_size + 5 + text_surf.get_width() + PyGUI.PADDING
|
||
if needed_width > context['width']:
|
||
context['width'] = needed_width
|
||
|
||
return new_value
|