diff --git a/pygame-imgui/__pycache__/pygui.cpython-311.pyc b/pygame-imgui/__pycache__/pygui.cpython-311.pyc new file mode 100644 index 0000000..c9df036 Binary files /dev/null and b/pygame-imgui/__pycache__/pygui.cpython-311.pyc differ diff --git a/pygame-imgui/__pycache__/pygui_pygame_backend.cpython-311.pyc b/pygame-imgui/__pycache__/pygui_pygame_backend.cpython-311.pyc new file mode 100644 index 0000000..aa31850 Binary files /dev/null and b/pygame-imgui/__pycache__/pygui_pygame_backend.cpython-311.pyc differ diff --git a/pygame-imgui/example.py b/pygame-imgui/example.py index e69de29..ef3e189 100644 --- a/pygame-imgui/example.py +++ b/pygame-imgui/example.py @@ -0,0 +1,87 @@ +import pygame +import sys +import math +import pygui +from pygui_pygame_backend import Render, Rect, get_mouse_pos, get_mouse_pressed, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION +import pygui_pygame_backend as backend + +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Bouncing Ball with Enhanced PyGUI") +clock = pygame.time.Clock() + +# Create the backend renderer and assign the backend to PyGUI. +renderer = Render(screen) +pygui.PyGUI.set_backend(backend, renderer) + +# --------------------- +# Ball Simulation +# --------------------- +class Ball: + def __init__(self, pos, velocity, radius, color): + self.pos = list(pos) + self.velocity = list(velocity) + self.radius = radius + self.color = color + + def update(self): + self.pos[0] += self.velocity[0] + self.pos[1] += self.velocity[1] + if self.pos[0] - self.radius < 0 or self.pos[0] + self.radius > WIDTH: + self.velocity[0] = -self.velocity[0] + if self.pos[1] - self.radius < 0 or self.pos[1] + self.radius > HEIGHT: + self.velocity[1] = -self.velocity[1] + + def draw(self, surface): + pygame.draw.circle(surface, self.color, (int(self.pos[0]), int(self.pos[1])), int(self.radius)) + +ball = Ball((WIDTH // 2, HEIGHT // 2), (4, 3), 30, (255, 100, 50)) +ball_speed = 4.0 +ball_direction = 0.0 +ball_radius = 30.0 +ball_color_r = 255.0 +ball_color_g = 100.0 +ball_color_b = 50.0 +bounce_enabled = True + +running = True +while running: + dt = clock.tick(60) / 1000.0 + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + pygui.PyGUI.handle_event(event) + + screen.fill((30, 30, 30)) + + # Draw a draggable, auto–sizing GUI window. + pygui.PyGUI.Begin("Ball Controls", 10, 10) + ball_speed = pygui.PyGUI.Slider("Speed", ball_speed, 1, 10) + ball_direction = pygui.PyGUI.Slider("Direction", ball_direction, 0, 360) + ball_radius = pygui.PyGUI.Slider("Radius", ball_radius, 10, 100) + ball_color_r = pygui.PyGUI.Slider("Red", ball_color_r, 0, 255) + ball_color_g = pygui.PyGUI.Slider("Green", ball_color_g, 0, 255) + ball_color_b = pygui.PyGUI.Slider("Blue", ball_color_b, 0, 255) + bounce_enabled = pygui.PyGUI.Checkbox("Bounce", bounce_enabled) + if pygui.PyGUI.Button("Reset Position"): + ball.pos = [WIDTH // 2, HEIGHT // 2] + pygui.PyGUI.End() + + rad = math.radians(ball_direction) + ball.velocity[0] = ball_speed * math.cos(rad) + ball.velocity[1] = ball_speed * math.sin(rad) + ball.radius = ball_radius + ball.color = (int(ball_color_r), int(ball_color_g), int(ball_color_b)) + if not bounce_enabled: + ball.velocity[0] *= 0.5 + ball.velocity[1] *= 0.5 + + ball.update() + ball.draw(screen) + + pygame.display.flip() + pygui.PyGUI.update() + +pygame.quit() +sys.exit() diff --git a/pygame-imgui/pygui.py b/pygame-imgui/pygui.py index e69de29..fb48453 100644 --- a/pygame-imgui/pygui.py +++ b/pygame-imgui/pygui.py @@ -0,0 +1,194 @@ +# 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] diff --git a/pygame-imgui/pygui_pygame_backend.py b/pygame-imgui/pygui_pygame_backend.py new file mode 100644 index 0000000..f098a01 --- /dev/null +++ b/pygame-imgui/pygui_pygame_backend.py @@ -0,0 +1,59 @@ +import pygame + +# Event type constants. +MOUSEBUTTONDOWN = pygame.MOUSEBUTTONDOWN +MOUSEBUTTONUP = pygame.MOUSEBUTTONUP +MOUSEMOTION = pygame.MOUSEMOTION + +class Render: + """ + Provides basic drawing functions via Pygame. + """ + def __init__(self, surface): + self.surface = surface + self.font = pygame.font.SysFont("Arial", 16) + + def draw_rect(self, color, rect, border=0): + pygame.draw.rect(self.surface, color, rect, border) + + def draw_text(self, text, pos, color=(255, 255, 255)): + text_surface = self.font.render(text, True, color) + self.surface.blit(text_surface, pos) + + def draw_line(self, color, start_pos, end_pos, width=1): + pygame.draw.line(self.surface, color, start_pos, end_pos, width) + + def draw_circle(self, color, center, radius, border=0): + pygame.draw.circle(self.surface, color, center, radius, border) + +class Rect: + """ + A simple rectangle wrapper. + """ + def __init__(self, x, y, width, height): + self.rect = pygame.Rect(x, y, width, height) + + @property + def x(self): + return self.rect.x + + @property + def y(self): + return self.rect.y + + @property + def width(self): + return self.rect.width + + @property + def height(self): + return self.rect.height + + def collidepoint(self, pos): + return self.rect.collidepoint(pos) + +def get_mouse_pos(): + return pygame.mouse.get_pos() + +def get_mouse_pressed(): + return pygame.mouse.get_pressed()