diff --git a/.gitignore b/.gitignore index 1fb64dc..ef08f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # .idea files .idea* + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/ui/button.py b/ui/button.py new file mode 100644 index 0000000..8c2f375 --- /dev/null +++ b/ui/button.py @@ -0,0 +1,221 @@ +import pygame +from math import floor + + +class Button: + # constructor that can be used to set parameters of Button instance + def __init__( + self, + position, + dimensions, + text="", + box_color=(195, 195, 195), + font=pygame.font.get_default_font(), + font_color=(50, 50, 50), + font_size=None, + is_visible=True, + is_active=True, + fit_text=True, + outline=True, + outline_thickness=None, + outline_color=(50, 50, 50), + box_transform_hover=1.16, + box_transform_inactive=0.9, + font_transform_hover=1.24, + font_transform_inactive=0.85 + ): + # extracting and setting values + smaller_dimension = min(dimensions) + width, height = dimensions + + # counting default outline thickness + default_outline_thickness = (smaller_dimension + (width + height) / 4) / 50 + + # setting attributes + self.position = position + self.dimensions = dimensions + self.text = text + self.box_color = _get_proper_rgb(box_color) + self.font_color = _get_proper_rgb(font_color) + self.font_size = _return_value_or_default(font_size, floor(smaller_dimension / 2)) + self.font = pygame.font.SysFont(font, self.font_size) + self.is_visible = is_visible + self.is_active = is_active + self.fit_text = fit_text + self.outline = outline + self.outline_thickness = _return_value_or_default(outline_thickness, default_outline_thickness) + self.outline_color = _get_proper_rgb(outline_color) + self.box_transform_hover = box_transform_hover + self.box_transform_inactive = box_transform_inactive + self.font_transform_hover = font_transform_hover + self.font_transform_inactive = font_transform_inactive + + # rendering text to get it's width and height + rendered_text = self.font.render(text, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_text.get_width() > self.dimensions[0]: + self.font_size = floor(self.font_size / (rendered_text.get_width() / width + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + # counting colors on: hover, inactive + self.box_hover_color = _get_proper_rgb(tuple(_transform(self.box_color, self.box_transform_hover))) + self.box_inactive_color = _get_proper_rgb(tuple(_transform(self.box_color, self.box_transform_inactive))) + self.font_hover_color = _get_proper_rgb(tuple(_transform(self.font_color, self.font_transform_hover))) + self.font_inactive_color = _get_proper_rgb(tuple(_transform(self.font_color, self.font_transform_inactive))) + + # draws the Button instance + def draw(self, window, mouse_position): + # if is_visible=True - drawing Button + if self.is_visible: + # extracting and setting values from attributes + box_x, box_y = self.position + width, height = self.dimensions + + # if outline=True - drawing an outline + if self.outline: + padding = self.outline_thickness + outline_coordinates = (box_x - padding, box_y - padding, width + 2 * padding, height + 2 * padding) + pygame.draw.rect(window, self.outline_color, outline_coordinates) + + # setting values accordingly to InputBox'es state + # is active and mouse is not over + if not self.is_over(mouse_position) and self.is_active: + pygame.draw.rect(window, self.box_color, (box_x, box_y, width, height)) + text_color = self.font_color + + # is active and mouse is over + elif self.is_active: + pygame.draw.rect(window, self.box_hover_color, (box_x, box_y, width, height)) + text_color = self.font_hover_color + + # is not active + else: + pygame.draw.rect(window, self.box_inactive_color, (box_x, box_y, width, height)) + text_color = self.font_inactive_color + + # rendering text and counting its coordinates + rendered_text = self.font.render(self.text, True, text_color) + rendered_text_x = box_x + width / 2 - rendered_text.get_width() / 2 + rendered_text_y = box_y + height / 2 - rendered_text.get_height() / 2 + + # drawing the text on the coordinates + window.blit(rendered_text, (rendered_text_x, rendered_text_y)) + + # returns True if the Button is clicked, or False otherwise + def is_clicked(self, mouse_position, events): + # if is_active=True and mouse is over the Button - checking if mouse is clicked + if self.is_active and self.is_over(mouse_position): + + # if mouse is clicked returning True, otherwise False + for event in events: + if event.type == pygame.MOUSEBUTTONUP: + return True + + return False + + # checks if a position is over the InputBox and returns True or False + def is_over(self, position): + mouse_x, mouse_y = position + button_x, button_y = self.position + width, height = self.dimensions + + return button_x <= mouse_x <= (button_x + width) and button_y <= mouse_y <= (button_y + height) + + # returns text value + def get_text(self): + return self.text + + # sets chosen coordinates + def set_coordinates(self, position=None, dimensions=None, outline_thickness=None): + if position is not None: + self.position = position + + if dimensions is not None: + self.dimensions = dimensions + + if outline_thickness is not None: + self.outline_thickness = outline_thickness + + # sets text attribute + def set_text(self, text): + self.text = text + + # sets chosen font attributes + def set_font(self, font=None, font_size=None): + if font_size is not None: + self.font_size = font_size + + # rendering text to get it's width and height + rendered_text = self.font.render(self.text, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_text.get_width() > self.dimensions[0]: + self.font_size = floor(self.font_size / (rendered_text.get_width() / self.dimensions[0] + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + if font is not None: + self.font = pygame.font.SysFont(font, self.font_size) + + # sets chosen color attributes + def set_colors( + self, + box_color=None, + font_color=None, + outline_color=None, + box_transform_hover=None, + box_transform_inactive=None, + font_transform_hover=None, + font_transform_inactive=None + ): + if box_color is not None: + self.box_color = _get_proper_rgb(box_color) + + if font_color is not None: + self.font_color = _get_proper_rgb(font_color) + + if outline_color is not None: + self.outline_color = _get_proper_rgb(outline_color) + + if box_transform_hover is not None: + self.box_transform_hover = box_transform_hover + + if box_transform_inactive is not None: + self.box_transform_inactive = box_transform_inactive + + if font_transform_hover is not None: + self.font_transform_hover = font_transform_hover + + if font_transform_inactive is not None: + self.font_transform_inactive = font_transform_inactive + + self.box_hover_color = _get_proper_rgb(tuple(_transform(self.box_color, self.box_transform_hover))) + self.box_inactive_color = _get_proper_rgb(tuple(_transform(self.box_color, self.box_transform_inactive))) + self.font_hover_color = _get_proper_rgb(tuple(_transform(self.font_color, self.font_transform_hover))) + self.font_inactive_color = _get_proper_rgb(tuple(_transform(self.font_color, self.font_transform_inactive))) + + # sets chosen flags + def set_flags(self, is_visible=None, is_active=None, outline=None): + if is_visible is not None: + self.is_visible = is_visible + + if is_active is not None: + self.is_active = is_active + + if outline is not None: + self.outline = outline + + +def _get_proper_rgb(x): + r, g, b = x + r, g, b = max(0, min(255, floor(r))), max(0, min(255, floor(g))), max(0, min(255, floor(b))) + + return tuple((r, g, b)) + + +def _transform(iterable, transform_value): + return [x * transform_value for x in iterable] + + +def _return_value_or_default(value, default): + return default if value is None else value diff --git a/ui/input_box.py b/ui/input_box.py new file mode 100644 index 0000000..95f3f36 --- /dev/null +++ b/ui/input_box.py @@ -0,0 +1,377 @@ +import pygame +from math import floor + + +class InputBox: + backspace_tick = 0 + backspace_deleted_streak = 0 + backspace_breakpoint = 6 + deleting_speeds = [12, 4] + + # constructor that can be used to set parameters of InputBox instance + def __init__( + self, + position, + dimensions, + input_box_position=None, + input_box_dimensions=None, + label="", + user_input="", + box_color=(195, 195, 195), + input_box_color=(225, 245, 245), + inner_box_color=(205, 205, 205), + bottom_strip_color=(180, 180, 180), + font=pygame.font.get_default_font(), + font_color=(50, 50, 50), + font_size=None, + is_visible=True, + is_active=True, + input_centered=False, + fit_text=True, + outline=True, + outline_thickness=None, + outline_color=(50, 50, 50), + input_box_transform_hover=1.07, + input_box_transform_selected=2, + input_box_transform_inactive=0.9 + ): + # extracting and setting values + smaller_dimension = min(dimensions) + box_x, box_y = position + width, height = dimensions + + # picking default input_box position and dimensions depending on the boxes width + if width < 70: + default_input_box_position = (box_x + width / 20, box_y + height / 2) + default_input_box_dimensions = (width - width / 10, 3 * height / 10) + + else: + default_input_box_position = (box_x + width / 12, box_y + height / 2) + default_input_box_dimensions = (width - width / 6, 3 * height / 10) + + # counting default outline thickness + default_outline_thickness = (smaller_dimension + (width + height) / 4) / 50 + + # setting attributes + self.position = position + self.dimensions = dimensions + self.input_box_position = _return_value_or_default(input_box_position, default_input_box_position) + self.input_box_dimensions = _return_value_or_default(input_box_dimensions, default_input_box_dimensions) + self.label = label + self.user_input = user_input + self.box_color = _get_proper_rgb(box_color) + self.ib_color = _get_proper_rgb(input_box_color) + self.inner_box_color = _get_proper_rgb(inner_box_color) + self.bottom_strip_color = _get_proper_rgb(bottom_strip_color) + self.font_color = _get_proper_rgb(font_color) + self.font_size = _return_value_or_default(font_size, floor(smaller_dimension / 2.8)) + self.font = pygame.font.SysFont(font, self.font_size) + self.input_font_size = max(15, floor(self.input_box_dimensions[1] - 2)) + self.input_font = pygame.font.SysFont(font, self.input_font_size) + self.is_visible = is_visible + self.is_active = is_active + self.is_selected = False + self.input_centered = input_centered + self.fit_text = fit_text + self.outline = outline + self.outline_thickness = _return_value_or_default(outline_thickness, default_outline_thickness) + self.outline_color = outline_color + self.ib_transform_hover = input_box_transform_hover + self.ib_transform_selected = input_box_transform_selected + self.ib_transform_inactive = input_box_transform_inactive + self.backspace_pressed_down = False + + # rendering label to get it's width and height + rendered_label = self.font.render(label, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_label.get_width() > self.input_box_dimensions[0]: + self.font_size = floor(self.font_size / (rendered_label.get_width() / width + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + # counting colors on: hover, inactive, selected + self.ib_hover_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_hover))) + self.ib_inactive_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_inactive))) + self.ib_selected_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_selected))) + + # draws, updates and selects on mouse click the InputBox instance + def run(self, window, mouse_position, events): + self.select_on_click(mouse_position, events) + self.draw(window, mouse_position) + self.update(events) + + # draws an InputBox instance (doesn't run interactions [for everything use run(self, ...) method]) + def draw(self, window, mouse_position): + # if is_visible=True drawing the InputBox + if self.is_visible: + # extracting and setting values from attributes + box_x, box_y = self.position + width, height = self.dimensions + input_box_x, input_box_y = self.input_box_position + input_width, input_height = self.input_box_dimensions + + # if outline=True - drawing an outline + if self.outline: + padding = self.outline_thickness + outline_coordinates = (box_x - padding, box_y - padding, width + 2 * padding, height + 2 * padding) + pygame.draw.rect(window, self.outline_color, outline_coordinates) + + # setting "transparent" background color + text_background_color = self.ib_color + + # setting values accordingly to InputBox'es state + # is selected + if self.is_selected: + input_box_color = self.ib_selected_color + text_background_color = (150, 255, 255) + + # is active and mouse is not over + elif not self.is_over(mouse_position) and self.is_active: + input_box_color = self.ib_color + + # is active and mouse is over + elif self.is_active: + input_box_color = self.ib_hover_color + + # is not active + else: + input_box_color = self.ib_inactive_color + + # drawing outer box + pygame.draw.rect(window, self.box_color, (box_x, box_y, width, height)) + + # drawing inner (upper) box + pygame.draw.rect(window, + self.inner_box_color, + (input_box_x, box_y + height / 10, input_width, height - input_height - height / 2.5) + ) + + # drawing input (lower) box + pygame.draw.rect(window, input_box_color, (input_box_x, input_box_y, input_width, input_height)) + + # drawing bottom strip + pygame.draw.rect(window, self.bottom_strip_color, (box_x, box_y + 9 * height / 10, width, 1 * height / 10)) + + # rendering label and counting its coordinates + rendered_label = self.font.render(self.label, True, self.font_color) + rendered_label_x = box_x + width / 2 - rendered_label.get_width() / 2 + rendered_label_y = box_y + height / 4 - rendered_label.get_height() / 2 + + # drawing the label on the coordinates + window.blit(rendered_label, (rendered_label_x, rendered_label_y)) + + # rendering input + rendered_input_text = self.input_font.render(self.user_input, True, self.font_color, text_background_color) + + # picking input coordinates on center or not depending on input_centered flag value + if self.input_centered: + rendered_input_text_x = input_box_x + input_width / 2 - rendered_input_text.get_width() / 2 + rendered_input_text_y = input_box_y + input_height / 2 - rendered_input_text.get_height() / 2 + + else: + rendered_input_text_x = input_box_x + 2 + rendered_input_text_y = input_box_y + input_height / 2 - rendered_input_text.get_height() / 2 + + # drawing the input on the coordinates + window.blit(rendered_input_text, (rendered_input_text_x, rendered_input_text_y)) + + # updates InputBox'es attributes if user types something in it + def update(self, events): + # if the InputBox is selected - updating the input text + if self.is_selected: + self._delete_characters_if_backspace_pressed() + + # rendering input to get it's width and height + rendered_input = self.input_font.render(self.user_input, True, self.font_color, self.ib_inactive_color) + + # deleting or adding characters depending on user's input + for event in events: + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_BACKSPACE: + if not self.backspace_pressed_down: + self.user_input = self.user_input[:-1] + self.backspace_pressed_down = True + + elif event.key == pygame.K_RETURN: + self.is_selected = False + + # if text isn't too long - adding a character + elif rendered_input.get_width() + 10 < self.input_box_dimensions[0]: + self.user_input += event.unicode + + elif event.type == pygame.KEYUP: + if event.key == pygame.K_BACKSPACE: + self.backspace_pressed_down = False + self.backspace_tick = 0 + self.backspace_deleted_streak = 0 + + # checks if the InputBox is clicked and changes is_selected to True or False + def select_on_click(self, mouse_position, events): + # if is active set is_selected attribute accordingly + if self.is_active: + for event in events: + # if mouse is clicked set as selected if mouse is over, or set as not selected otherwise + if event.type == pygame.MOUSEBUTTONUP: + self.set_is_selected(self.is_over(mouse_position)) + + # checks if a position is over the InputBox and returns True or False + def is_over(self, position): + mouse_x, mouse_y = position + button_x, button_y = self.position + width, height = self.dimensions + + return button_x <= mouse_x <= (button_x + width) and button_y <= mouse_y <= (button_y + height) + + # returns InputBox'es label + def get_label(self): + return self.label + + # returns InputBox'es user's input + def get_input(self): + return self.user_input + + # sets chosen coordinates + def set_space( + self, + position=None, + dimensions=None, + input_box_position=None, + input_box_dimensions=None, + outline_thickness=None + ): + if position is not None: + self.position = position + + if dimensions is not None: + self.dimensions = dimensions + + if input_box_position is not None: + self.input_box_position = input_box_position + + if input_box_dimensions is not None: + self.input_box_dimensions = input_box_dimensions + + if outline_thickness is not None: + self.outline_thickness = outline_thickness + + # sets chosen text attributes + def set_texts(self, label=None, user_input=None): + if label is not None: + self.label = label + + if user_input is not None: + self.user_input = user_input + + # sets chosen font attributes + def set_font(self, font=None, font_size=None): + if font_size is not None: + self.font_size = font_size + + # rendering text to get it's width and height + rendered_text = self.font.render(self.user_input, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_text.get_width() > self.dimensions[0]: + self.font_size = floor(self.font_size / (rendered_text.get_width() / self.dimensions[0] + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + if font is not None: + self.font = pygame.font.SysFont(font, self.font_size) + + # sets chosen color attributes + def set_colors( + self, + box_color=None, + input_box_color=None, + inner_box_color=None, + bottom_strip_color=None, + font_color=None, + outline_color=None, + input_box_transform_hover=None, + input_box_transform_selected=None, + input_box_transform_inactive=None + ): + if box_color is not None: + self.box_color = _get_proper_rgb(box_color) + + if input_box_color is not None: + self.ib_color = _get_proper_rgb(input_box_color) + + if inner_box_color is not None: + self.inner_box_color = _get_proper_rgb(inner_box_color) + + if bottom_strip_color is not None: + self.bottom_strip_color = _get_proper_rgb(bottom_strip_color) + + if font_color is not None: + self.font_color = _get_proper_rgb(font_color) + + if outline_color is not None: + self.outline_color = _get_proper_rgb(outline_color) + + if input_box_transform_hover is not None: + self.ib_transform_hover = input_box_transform_hover + + if input_box_transform_selected is not None: + self.ib_transform_selected = input_box_transform_selected + + if input_box_transform_inactive is not None: + self.ib_transform_inactive = input_box_transform_inactive + + self.ib_hover_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_hover))) + self.ib_inactive_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_inactive))) + self.ib_selected_color = _get_proper_rgb(tuple(_transform(self.ib_color, self.ib_transform_selected))) + + # sets chosen flag attributes + def set_flags(self, is_visible=None, is_active=None, input_centered=None, fit_text=None, outline=None): + if is_visible is not None: + self.is_visible = is_visible + + if is_active is not None: + self.is_active = is_active + + if input_centered is not None: + self.input_centered = input_centered + + if fit_text is not None: + self.fit_text = fit_text + + if outline is not None: + self.outline = outline + + # sets is_selected with a given value + def set_is_selected(self, is_selected): + self.is_selected = is_selected + + def _delete_characters_if_backspace_pressed(self): + if self.backspace_pressed_down and \ + (self.backspace_tick + 1) % self.deleting_speeds[0] == 0 and \ + self.backspace_deleted_streak <= 6: + self.user_input = self.user_input[:-1] + self.backspace_tick = 0 + self.backspace_deleted_streak += 1 + + elif self.backspace_pressed_down and \ + (self.backspace_tick + 1) % self.deleting_speeds[1] == 0 and \ + self.backspace_deleted_streak > 6: + self.user_input = self.user_input[:-1] + self.backspace_tick = 0 + self.backspace_deleted_streak += 1 + + elif self.backspace_pressed_down: + self.backspace_tick += 1 + + +def _get_proper_rgb(x): + r, g, b = x + r, g, b = max(0, min(255, floor(r))), max(0, min(255, floor(g))), max(0, min(255, floor(b))) + + return tuple((r, g, b)) + + +def _transform(iterable, transform_value): + return [x * transform_value for x in iterable] + + +def _return_value_or_default(value, default): + return default if value is None else value diff --git a/ui/text_box.py b/ui/text_box.py new file mode 100644 index 0000000..e8d71d5 --- /dev/null +++ b/ui/text_box.py @@ -0,0 +1,137 @@ +import pygame +from math import floor + + +class TextBox: + # constructor that can be used to set parameters of TextBox instance + def __init__( + self, + position, + dimensions, + text="", + box_color=(200, 200, 200), + font=pygame.font.get_default_font(), + font_color=(0, 0, 0), + font_size=None, + is_visible=True, + fit_text=True, + outline=True, + outline_thickness=None, + outline_color=(50, 50, 50) + ): + # extracting and setting values + smaller_dimension = min(dimensions) + width, height = dimensions + + # counting default outline thickness + default_outline_thickness = (smaller_dimension + (width + height) / 4) / 50 + + # setting attributes + self.position = position + self.dimensions = dimensions + self.text = text + self.box_color = _get_proper_rgb(box_color) + self.font_color = _get_proper_rgb(font_color) + self.font_size = _return_value_or_default(font_size, floor(smaller_dimension / 2)) + self.font = pygame.font.SysFont(font, self.font_size) + self.is_visible = is_visible + self.fit_text = fit_text + self.outline = outline + self.outline_thickness = _return_value_or_default(outline_thickness, default_outline_thickness) + self.outline_color = _get_proper_rgb(outline_color) + + # rendering text to get it's width and height + rendered_text = self.font.render(text, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_text.get_width() > self.dimensions[0]: + self.font_size = floor(self.font_size / (rendered_text.get_width() / width + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + # draws the TextBox + def draw(self, window): + # if is_visible=True drawing the TextBox + if self.is_visible: + # extracting and setting values from attributes + box_x, box_y = self.position + width, height = self.dimensions + + # if outline=True - drawing an outline + if self.outline: + padding = self.outline_thickness + outline_coordinates = (box_x - padding, box_y - padding, width + 2 * padding, height + 2 * padding) + pygame.draw.rect(window, self.outline_color, outline_coordinates) + + # drawing the box + pygame.draw.rect(window, self.box_color, (box_x, box_y, width, height)) + + # rendering text and counting it's coordinates + rendered_text = self.font.render(self.text, True, self.font_color) + rendered_text_x = box_x + width/2 - rendered_text.get_width()/2 + rendered_text_y = box_y + height/2 - rendered_text.get_height()/2 + + # drawing the box on the coordinates + window.blit(rendered_text, (rendered_text_x, rendered_text_y)) + + # sets chosen coordinates + def set_coordinates(self, position=None, dimensions=None, outline_thickness=None): + if position is not None: + self.position = position + + if dimensions is not None: + self.dimensions = dimensions + + if outline_thickness is not None: + self.outline_thickness = outline_thickness + + # sets the TextBox's text + def set_text(self, text): + self.text = text + + # sets chosen font attributes + def set_font(self, font=None, font_size=None): + if font_size is not None: + self.font_size = font_size + + # rendering text to get it's width and height + rendered_text = self.font.render(self.text, True, (0, 0, 0)) + + # if text is out of bounds and fit_text=True - resizing text to fit the box + if self.fit_text and rendered_text.get_width() > self.dimensions[0]: + self.font_size = floor(self.font_size / (rendered_text.get_width() / self.dimensions[0] + 0.1)) + self.font = pygame.font.SysFont(font, self.font_size) + + if font is not None: + self.font = pygame.font.SysFont(font, self.font_size) + + # sets chosen color attributes + def set_colors(self, box_color=None, font_color=None, outline_color=None): + if box_color is not None: + self.box_color = _get_proper_rgb(box_color) + + if font_color is not None: + self.font_color = _get_proper_rgb(font_color) + + if outline_color is not None: + self.outline_color = outline_color + + # sets chosen flags + def set_flags(self, is_visible=None, outline=None): + if is_visible is not None: + self.is_visible = is_visible + + if outline is not None: + self.outline = outline + + +# returns values that are not out of range +def _get_proper_rgb(x): + r, g, b = x + r, g, b = max(0, min(255, floor(r))), max(0, min(255, floor(g))), max(0, min(255, floor(b))) + + return tuple((r, g, b)) + + +# returns value or default depending on if passed value is None or not +def _return_value_or_default(value, default): + return default if value is None else value