# pygui.py: Immediate–mode 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 one–click 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 backend’s 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 # Auto–resize 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 one–click toggling). PyGUI.prev_mouse = PyGUI.backend.get_mouse_pressed()[0]