small-projects/pygame-imgui-v2/imgui.py
2025-04-08 11:28:40 -05:00

257 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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