small-projects/pygame-imgui/pygui.py
2025-04-07 11:37:34 -05:00

195 lines
7.6 KiB
Python
Raw 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.

# pygui.py: Immediatemode GUI library.
# This file contains no direct Pygame calls—it relies entirely on functions supplied by the backend.
# The backend must supply: Rect, get_mouse_pos, get_mouse_pressed,
# and event type constants: MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION.
class Window:
TITLE_HEIGHT = 30
PADDING = 5
SPACING = 5
def __init__(self, title, x, y, width=None, height=None):
self.title = title
self.x = x
self.y = y
self.width = width if width is not None else 200
# Start with a minimal height if none is provided.
self.height = height if height is not None else (self.TITLE_HEIGHT + self.PADDING)
self.cursor_y = self.y + self.TITLE_HEIGHT + self.PADDING
self.dragging = False
self.drag_offset = (0, 0)
@property
def rect(self):
return Window.backend.Rect(self.x, self.y, self.width, self.height)
@property
def title_rect(self):
return Window.backend.Rect(self.x, self.y, self.width, self.TITLE_HEIGHT)
def handle_event(self, ev):
# ev is a dictionary with keys: type, button, pos.
if ev.get('type') == 'MOUSEBUTTONDOWN':
if ev.get('button') == 1 and self.title_rect.collidepoint(ev.get('pos')):
self.dragging = True
self.drag_offset = (ev.get('pos')[0] - self.x, ev.get('pos')[1] - self.y)
elif ev.get('type') == 'MOUSEBUTTONUP':
if ev.get('button') == 1:
self.dragging = False
elif ev.get('type') == 'MOUSEMOTION':
if self.dragging:
self.x = ev.get('pos')[0] - self.drag_offset[0]
self.y = ev.get('pos')[1] - self.drag_offset[1]
self.cursor_y = self.y + self.TITLE_HEIGHT + self.PADDING
def begin(self, render):
render.draw_rect((50, 50, 50), self.rect.rect)
render.draw_rect((70, 70, 70), self.title_rect.rect)
render.draw_text(self.title, (self.x + self.PADDING, self.y + 5))
self.cursor_y = self.y + self.TITLE_HEIGHT + self.PADDING
def layout(self):
return (self.x + self.PADDING, self.cursor_y)
def next(self, widget_height):
self.cursor_y += widget_height + self.SPACING
class PyGUI:
# These are assigned via set_backend.
backend = None
renderer = None
current_window = None
windows = []
active_slider = None # Stores (window_id, label, offset)
prev_mouse = False # For oneclick widget toggling
@staticmethod
def set_backend(backend, renderer):
"""
backend: an object that provides Rect, get_mouse_pos, get_mouse_pressed,
and event type constants: MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION.
renderer: an instance of the backends Render class.
"""
PyGUI.backend = backend
PyGUI.renderer = renderer
Window.backend = backend
@staticmethod
def Begin(title, x, y, width=None, height=None):
win = Window(title, x, y, width, height)
PyGUI.current_window = win
PyGUI.windows.append(win)
win.begin(PyGUI.renderer)
@staticmethod
def End():
if PyGUI.current_window:
win = PyGUI.current_window
# Autoresize window height to enclose all widgets.
win.height = win.cursor_y - win.y + win.PADDING
PyGUI.current_window = None
@staticmethod
def Label(text):
if not PyGUI.current_window:
return
win = PyGUI.current_window
pos = win.layout()
PyGUI.renderer.draw_text(text, pos)
win.next(20)
@staticmethod
def Button(text):
if not PyGUI.current_window:
return False
win = PyGUI.current_window
btn_rect = PyGUI.backend.Rect(win.x + win.PADDING, win.cursor_y, win.width - 2 * win.PADDING, 25)
PyGUI.renderer.draw_rect((100, 100, 100), btn_rect.rect)
PyGUI.renderer.draw_text(text, (btn_rect.x + 5, btn_rect.y + 5))
clicked = False
mouse_pos = PyGUI.backend.get_mouse_pos()
if btn_rect.collidepoint(mouse_pos):
PyGUI.renderer.draw_rect((150, 150, 150), btn_rect.rect, border=2)
if PyGUI.backend.get_mouse_pressed()[0]:
clicked = True
win.next(25)
return clicked
@staticmethod
def Slider(label, value, min_val, max_val):
if not PyGUI.current_window:
return value
win = PyGUI.current_window
pos = win.layout()
text = f"{label}: {value:.2f}"
PyGUI.renderer.draw_text(text, pos)
win.next(20)
slider_width = win.width - 2 * win.PADDING
slider_rect = PyGUI.backend.Rect(win.x + win.PADDING, win.cursor_y, slider_width, 10)
PyGUI.renderer.draw_rect((100, 100, 100), slider_rect.rect)
norm = (value - min_val) / (max_val - min_val)
knob_x = win.x + win.PADDING + norm * slider_width
knob_rect = PyGUI.backend.Rect(knob_x - 5, win.cursor_y - 5, 10, 20)
PyGUI.renderer.draw_rect((200, 200, 200), knob_rect.rect)
slider_id = (id(win), label)
mp = PyGUI.backend.get_mouse_pos()
mp_pressed = PyGUI.backend.get_mouse_pressed()[0]
# Start slider drag if none active and mouse is pressed inside slider_rect.
if PyGUI.active_slider is None and slider_rect.collidepoint(mp) and mp_pressed:
offset = mp[0] - knob_x
PyGUI.active_slider = (slider_id, offset)
if PyGUI.active_slider is not None and PyGUI.active_slider[0] == slider_id:
offset = PyGUI.active_slider[1]
rel = mp[0] - (win.x + win.PADDING) - offset
norm = max(0, min(rel / slider_width, 1))
value = min_val + norm * (max_val - min_val)
if not mp_pressed:
PyGUI.active_slider = None
win.next(20)
return value
@staticmethod
def Checkbox(label, value):
if not PyGUI.current_window:
return value
win = PyGUI.current_window
pos = win.layout()
box_rect = PyGUI.backend.Rect(win.x + win.PADDING, win.cursor_y, 20, 20)
PyGUI.renderer.draw_rect((100, 100, 100), box_rect.rect, border=2)
if value:
PyGUI.renderer.draw_rect((200, 200, 200), box_rect.rect)
PyGUI.renderer.draw_text(label, (box_rect.x + box_rect.width + 5, win.cursor_y))
new_value = value
mp = PyGUI.backend.get_mouse_pos()
mp_pressed = PyGUI.backend.get_mouse_pressed()[0]
# Toggle only on the transition (mouse pressed now but not in the previous frame).
if box_rect.collidepoint(mp) and mp_pressed and not PyGUI.prev_mouse:
new_value = not value
win.next(20)
return new_value
@staticmethod
def handle_event(event):
# Convert a backend event (e.g. a Pygame event) into a generic dictionary.
generic = {}
if event.type == PyGUI.backend.MOUSEBUTTONDOWN:
generic['type'] = 'MOUSEBUTTONDOWN'
generic['button'] = event.button
generic['pos'] = event.pos
elif event.type == PyGUI.backend.MOUSEBUTTONUP:
generic['type'] = 'MOUSEBUTTONUP'
generic['button'] = event.button
generic['pos'] = event.pos
elif event.type == PyGUI.backend.MOUSEMOTION:
generic['type'] = 'MOUSEMOTION'
generic['pos'] = event.pos
for win in PyGUI.windows:
win.handle_event(generic)
@staticmethod
def update():
# Call at end of frame to record the current mouse state (for oneclick toggling).
PyGUI.prev_mouse = PyGUI.backend.get_mouse_pressed()[0]