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