From a360202662f229eaa54f445907b7e6b414e66492 Mon Sep 17 00:00:00 2001 From: Manthan Ankolekar Date: Sat, 21 Jun 2025 07:54:59 +0530 Subject: [PATCH] Add dark mode feature with theme toggle - Implement comprehensive theme system with light and dark color schemes - Add 'Dark Mode: ON/OFF' toggle button showing current state - Update all UI components to support theme-aware rendering - Enhance ButtonBox class to support both image and text buttons - Improve text contrast and visibility in dark mode - Update README with dark mode documentation and usage instructions Features: - Dynamic theme switching without restart - Consistent color scheme across all components - Better visibility for sorting algorithm visualization - User-friendly toggle button with clear state indication --- README.md | 16 +++++++ src/display.py | 100 +++++++++++++++++++++++++++++++++-------- src/main.py | 119 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 187 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index d4c5f8d..815e80d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ Program made with Python and Pygame module for visualizing sorting algorithms \ Support this project by leaving a :star: +## Features + +- **Interactive Visualization**: Watch sorting algorithms in real-time +- **Multiple Algorithms**: Support for 25+ different sorting algorithms +- **Adjustable Parameters**: Control array size and animation speed +- **Dark Mode**: Toggle between light and dark themes for better viewing experience +- **User-Friendly Interface**: Easy-to-use controls and clear visual feedback + | | | | |:-------------------------:|:-------------------------:|:-------------------------:| |![](https://github.com/LucasPilla/Sorting-Algorithms-Visualizer/blob/master/res/bubble_sort.gif?raw=true) Bubble sort | ![](https://github.com/LucasPilla/Sorting-Algorithms-Visualizer/blob/master/res/bucket_sort.gif?raw=true) Bucket sort |![](https://github.com/LucasPilla/Sorting-Algorithms-Visualizer/blob/master/res/cocktail_sort.gif?raw=true) Cocktail sort | @@ -14,3 +22,11 @@ Support this project by leaving a :star: - Clone GitHub repository `git clone https://github.com/LucasPilla/Sorting-Algorithms-Visualizer.git` - Install requirements: `pip3 install -r requirements.txt` - Run: `python3 src/main.py` + +## Controls + +- **Size Input**: Enter the number of elements to sort (default: 100) +- **Delay Slider**: Adjust animation speed (lower = faster) +- **Algorithm Dropdown**: Select from 25+ sorting algorithms +- **Play/Stop Button**: Start or stop the sorting visualization +- **Dark Mode Toggle**: Switch between light and dark themes (shows "Dark Mode: OFF" or "Dark Mode: ON") diff --git a/src/display.py b/src/display.py index 4eb1869..4b206e5 100644 --- a/src/display.py +++ b/src/display.py @@ -11,6 +11,9 @@ def __init__(self, screen): def add_widget(self, widget_id, widget): self.widgets[widget_id] = widget + def clear_widgets(self): + self.widgets = {} + def get_widget_value(self, widget_id): return self.widgets[widget_id].get_value() @@ -38,14 +41,16 @@ def update(self, event): class InputBox(ABC, Box): - def __init__(self, rect, label, color, font): + def __init__(self, rect, label, color, font, theme=None): super().__init__(rect) self.label = label self.color = color self.font = font + self.theme = theme def render(self, screen): - label = self.font.render(self.label, True, self.color) + text_color = self.theme.get_color('text') if self.theme else self.color + label = self.font.render(self.label, True, text_color) screen.blit(label, (self.rect.x + (self.rect.w - label.get_width()) / 2, self.rect.y - 32)) pygame.draw.rect(screen, self.color, self.rect, 2) @@ -59,13 +64,20 @@ def set_value(self, value): class TextBox(InputBox): - def __init__(self, rect, label, color, font, text): - super().__init__(rect, label, color, font) + def __init__(self, rect, label, color, font, text, theme=None): + super().__init__(rect, label, color, font, theme) self.text = text def render(self, screen): super().render(screen) - surface = self.font.render(self.text, True, self.color) + # Fill background + if self.theme: + pygame.draw.rect(screen, self.theme.get_color('widget_background'), self.rect) + pygame.draw.rect(screen, self.color, self.rect, 2) + text_color = self.theme.get_color('text') + else: + text_color = self.color + surface = self.font.render(self.text, True, text_color) screen.blit(surface, surface.get_rect(center=self.rect.center)) def update(self, event): @@ -84,8 +96,8 @@ def set_value(self, value): class SlideBox(InputBox): - def __init__(self, rect, label, color, font): - super().__init__(rect, label, color, font) + def __init__(self, rect, label, color, font, theme=None): + super().__init__(rect, label, color, font, theme) self.start = self.rect.x + 6 self.end = self.rect.x + self.rect.w - 6 self.value = self.start @@ -93,6 +105,9 @@ def __init__(self, rect, label, color, font): def render(self, screen): super().render(screen) + # Fill background + if self.theme: + pygame.draw.rect(screen, self.theme.get_color('widget_background'), self.rect) pygame.draw.rect(screen, self.color, self.rect, 2) pygame.draw.line(screen, self.color, (self.start, self.rect.y + 25), (self.end, self.rect.y + 25), 2) pygame.draw.line(screen, self.color, (self.value, self.rect.y + 5), (self.value, self.rect.y + 45), 12) @@ -126,17 +141,49 @@ def set_value(self, value): self.value = self.start + value * (self.end - self.start) class ButtonBox(Box): - def __init__(self, rect, inactive_img_path, active_img_path): + def __init__(self, rect, inactive_img_path=None, active_img_path=None, theme=None, text=None): super().__init__(rect) - self.inactive_img = pygame.image.load(inactive_img_path) - self.inactive_img = pygame.transform.scale(self.inactive_img, (rect[2], rect[3])) - self.active_img = pygame.image.load(active_img_path) - self.active_img = pygame.transform.scale(self.active_img, (rect[2], rect[3])) + self.theme = theme + self.text = text + + if inactive_img_path and active_img_path: + self.inactive_img = pygame.image.load(inactive_img_path) + self.inactive_img = pygame.transform.scale(self.inactive_img, (rect[2], rect[3])) + self.active_img = pygame.image.load(active_img_path) + self.active_img = pygame.transform.scale(self.active_img, (rect[2], rect[3])) + self.is_image_button = True + else: + self.inactive_img = None + self.active_img = None + self.is_image_button = False + self.active = False + self.font = pygame.font.SysFont('Arial', 14) def render(self, screen): - img = self.active_img if self.active else self.inactive_img - screen.blit(img, (self.rect.x, self.rect.y)) + if self.is_image_button: + img = self.active_img if self.active else self.inactive_img + screen.blit(img, (self.rect.x, self.rect.y)) + else: + # Text button + if self.theme: + bg_color = self.theme.get_color('widget_background') + border_color = self.theme.get_color('widget_border') + text_color = self.theme.get_color('text') + else: + bg_color = (200, 200, 200) + border_color = (100, 100, 100) + text_color = (0, 0, 0) + + # Draw button background + pygame.draw.rect(screen, bg_color, self.rect) + pygame.draw.rect(screen, border_color, self.rect, 2) + + # Draw text + if self.text: + text_surface = self.font.render(self.text, True, text_color) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) def update(self, event): super().update(event) @@ -145,20 +192,27 @@ def update(self, event): def get_value(self): return self.active - + def set_value(self, value): self.active = value + + def set_text(self, text): + self.text = text class DropdownBox(InputBox): VISIBLE_OPTIONS = 8 - def __init__(self, rect, label, color, font, options, options_background_color): - super().__init__(rect, label, color, font) + def __init__(self, rect, label, color, font, options, options_background_color, theme=None): + super().__init__(rect, label, color, font, theme) self.openDropdown = False self.options = options self.options_background_color = options_background_color + + # Update background color based on theme + if theme: + self.options_background_color = theme.get_color('widget_background') self.dropdown_rect = pygame.Rect( self.rect.x, @@ -172,9 +226,17 @@ def __init__(self, rect, label, color, font, options, options_background_color): def render(self, screen): super().render(screen) + + # Fill background + if self.theme: + pygame.draw.rect(screen, self.theme.get_color('widget_background'), self.rect) + pygame.draw.rect(screen, self.color, self.rect, 2) + text_color = self.theme.get_color('text') + else: + text_color = self.color # Render the selected option in the input box - option_text = self.font.render(self.options[self.selected_option], 1, self.color) + option_text = self.font.render(self.options[self.selected_option], 1, text_color) screen.blit(option_text, option_text.get_rect(center=self.rect.center)) if self.openDropdown: @@ -192,7 +254,7 @@ def render(self, screen): pygame.draw.rect(screen, self.options_background_color, rect) pygame.draw.rect(screen, self.color, rect, 1) - option_text = self.font.render(self.options[index], 1, self.color) + option_text = self.font.render(self.options[index], 1, text_color) screen.blit(option_text, option_text.get_rect(center=rect.center)) # Render the scrollbar diff --git a/src/main.py b/src/main.py index 3a362de..cea489a 100644 --- a/src/main.py +++ b/src/main.py @@ -11,34 +11,90 @@ # Font baseFont = pygame.font.SysFont('Arial', 24) -# Colors -grey = (100, 100, 100) -green = (125, 240, 125) -white = (250, 250, 250) -red = (255, 50, 50) -black = (0, 0, 0) -blue = (50, 50, 255) +# Theme system +class Theme: + def __init__(self): + self.is_dark_mode = False + self.light_theme = { + 'background': (250, 250, 250), + 'widget_border': (100, 100, 100), + 'widget_background': (250, 250, 250), + 'text': (0, 0, 0), + 'bar_default': (100, 100, 100), + 'bar_comparing': (255, 50, 50), + 'bar_pivot': (50, 50, 255), + 'bar_sorted': (125, 240, 125) + } + self.dark_theme = { + 'background': (30, 30, 30), + 'widget_border': (150, 150, 150), + 'widget_background': (50, 50, 50), + 'text': (255, 255, 255), + 'bar_default': (120, 120, 120), + 'bar_comparing': (255, 100, 100), + 'bar_pivot': (100, 100, 255), + 'bar_sorted': (100, 200, 100) + } + + def get_color(self, color_name): + theme = self.dark_theme if self.is_dark_mode else self.light_theme + return theme.get(color_name, (255, 255, 255)) + + def toggle_theme(self): + self.is_dark_mode = not self.is_dark_mode + +# Global theme instance +theme = Theme() + +# Legacy color variables for compatibility (will be updated dynamically) +grey = theme.get_color('widget_border') +green = theme.get_color('bar_sorted') +white = theme.get_color('background') +red = theme.get_color('bar_comparing') +black = theme.get_color('text') +blue = theme.get_color('bar_pivot') pygame.display.set_caption('Sorting Algorithms Visualizer') screen = pygame.display.set_mode((900, 500)) window = Window(screen) -window.add_widget( - widget_id = 'size_input', - widget = TextBox((30, 440, 100, 50), 'Size', grey, baseFont, '100') -) -window.add_widget( - widget_id='delay_slider', - widget=SlideBox((140, 440, 150, 50), 'Delay', grey, baseFont) -) -window.add_widget( - widget_id = 'algorithm_input', - widget = DropdownBox((300, 440, 200, 50), 'Algorithm', grey, baseFont, list(algorithmsDict.keys()), white) -) -window.add_widget( - widget_id = 'play_button', - widget = ButtonBox((510, 445, 40, 40), 'res/playButton.png', 'res/stopButton.png') -) +def update_colors(): + """Update color variables based on current theme""" + global grey, green, white, red, black, blue + grey = theme.get_color('widget_border') + green = theme.get_color('bar_sorted') + white = theme.get_color('background') + red = theme.get_color('bar_comparing') + black = theme.get_color('text') + blue = theme.get_color('bar_pivot') + +def create_widgets(): + """Create or recreate widgets with current theme colors""" + window.clear_widgets() + + window.add_widget( + widget_id = 'size_input', + widget = TextBox((30, 440, 100, 50), 'Size', theme.get_color('widget_border'), baseFont, '100', theme) + ) + window.add_widget( + widget_id='delay_slider', + widget=SlideBox((140, 440, 150, 50), 'Delay', theme.get_color('widget_border'), baseFont, theme) + ) + window.add_widget( + widget_id = 'algorithm_input', + widget = DropdownBox((300, 440, 200, 50), 'Algorithm', theme.get_color('widget_border'), baseFont, list(algorithmsDict.keys()), theme.get_color('widget_background'), theme) + ) + window.add_widget( + widget_id = 'play_button', + widget = ButtonBox((510, 445, 40, 40), 'res/playButton.png', 'res/stopButton.png', theme) ) + dark_mode_text = 'Dark Mode: ON' if theme.is_dark_mode else 'Dark Mode: OFF' + window.add_widget( + widget_id = 'theme_toggle', + widget = ButtonBox((560, 445, 120, 40), None, None, theme, text=dark_mode_text) + ) + +# Initialize widgets +create_widgets() def drawBars(screen, array, redBar1, redBar2, blueBar1, blueBar2, greenRows = {}): '''Draw the bars and control their colors''' @@ -48,10 +104,10 @@ def drawBars(screen, array, redBar1, redBar2, blueBar1, blueBar2, greenRows = {} ceil_width = math.ceil(bar_width) for num in range(numBars): - if num in (redBar1, redBar2) : color = red - elif num in (blueBar1, blueBar2): color = blue - elif num in greenRows : color = green - else : color = grey + if num in (redBar1, redBar2) : color = theme.get_color('bar_comparing') + elif num in (blueBar1, blueBar2): color = theme.get_color('bar_pivot') + elif num in greenRows : color = theme.get_color('bar_sorted') + else : color = theme.get_color('bar_default') pygame.draw.rect(screen, color, (num * bar_width, 400 - array[num], ceil_width, array[num])) def main(): @@ -63,12 +119,17 @@ def main(): last_iteration = 0 while running: - screen.fill(white) + screen.fill(theme.get_color('background')) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False - window.update(event) + window.update(event) # Check for theme toggle + if window.get_widget_value('theme_toggle'): + theme.toggle_theme() + update_colors() + create_widgets() + window.set_widget_value('theme_toggle', False) # Reset toggle button # Get delay in seconds delay = window.get_widget_value('delay_slider') / 10