small-projects/pygame-imgui-v2/imgui.py

257 lines
11 KiB
Python
Raw Normal View History

2025-04-08 16:28:40 +00:00
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, autosizing 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