Skip to content

Add dark mode feature with theme toggle #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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")
100 changes: 81 additions & 19 deletions src/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)

Expand All @@ -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):
Expand All @@ -84,15 +96,18 @@ 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
self.dragging = False # Track if the user is dragging the slider

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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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
Expand Down
119 changes: 90 additions & 29 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'''
Expand All @@ -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():
Expand All @@ -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
Expand Down