def redraw_from_text_block(self): """ Redraws the final parts of the text box element that don't include redrawing the actual text. Useful if we've just moved the position of the text (say, with a scroll bar) without actually changing the text itself. """ if self.rect.width <= 0 or self.rect.height <= 0: return if self.scroll_bar is not None: height_adjustment = int( self.scroll_bar.start_percentage * self.formatted_text_block.final_dimensions[1]) else: height_adjustment = 0 drawable_area_size = (max( 1, (self.rect[2] - (self.padding[0] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset))), max(1, (self.rect[3] - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) drawable_area = pygame.Rect((0, height_adjustment), drawable_area_size) new_image = pygame.surface.Surface(self.rect.size, flags=pygame.SRCALPHA, depth=32) new_image.fill(pygame.Color(0, 0, 0, 0)) basic_blit(new_image, self.background_surf, (0, 0)) basic_blit( new_image, self.formatted_text_block.block_sprite, (self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset, self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset), drawable_area) self.set_image(new_image)
def set_image(self, new_image: Union[pygame.surface.Surface, None]): """ Wraps setting the image variable of this element so that we also set the current image clip on the image at the same time. :param new_image: The new image to set. """ if self.get_image_clipping_rect() is not None and new_image is not None: self._pre_clipped_image = new_image if (self.get_image_clipping_rect().width == 0 and self.get_image_clipping_rect().height == 0): self.image = self.ui_manager.get_universal_empty_surface() else: self.image = pygame.surface.Surface(self._pre_clipped_image.get_size(), flags=pygame.SRCALPHA, depth=32) self.image.fill(pygame.Color('#00000000')) basic_blit(self.image, self._pre_clipped_image, self.get_image_clipping_rect(), self.get_image_clipping_rect()) else: self.image = new_image.copy() if new_image is not None else None self._pre_clipped_image = None
def redraw_from_chunks(self, text_effect: Union[TextBoxEffect, None]): """ Redraw only the last part of text block starting from the already complete styled and word wrapped StyledChunks. :param text_effect: The text effect to use when redrawing. """ final_alpha = text_effect.get_final_alpha() if text_effect else 255 self.block_sprite = pygame.surface.Surface((self.width, self.height), flags=pygame.SRCALPHA, depth=32) self.block_sprite.fill(pygame.Color('#00000000')) if isinstance(self.bg_colour, ColourGradient): self.block_sprite.fill(pygame.Color("#FFFFFFFF")) self.bg_colour.apply_gradient_to_surface(self.block_sprite) else: self.block_sprite.fill(self.bg_colour) for text_line in self.lines: for chunk in text_line.chunks: if self.block_sprite is not None: basic_blit(self.block_sprite, chunk.rendered_chunk, chunk.rect) self.block_sprite.set_alpha(final_alpha)
def finalise_images_and_text(self, image_state_str: str, state_str: str, text_colour_state_str: str, text_shadow_colour_state_str: str, add_text: bool): """ Rebuilds any text or image used by a specific state in the drawable shape. Effectively this means adding them on top of whatever is already in the state's surface. As such it should generally be called last in the process of building up a finished drawable shape state. :param add_text: :param image_state_str: image ID of the state we are going to be adding images and text to. :param state_str: normal ID of the state we are going to be adding images and text to. :param text_colour_state_str: text ID of the state we are going to be adding images and text to. :param text_shadow_colour_state_str: text shadow ID of the state we are going to be adding images and text to. """ # Draw any themed images if image_state_str in self.theming and self.theming[ image_state_str] is not None: image_rect = self.theming[image_state_str].get_rect() image_rect.center = (int(self.containing_rect.width / 2), int(self.containing_rect.height / 2)) basic_blit(self.states[state_str].surface, self.theming[image_state_str], image_rect) if add_text: self.finalise_text(state_str, text_colour_state_str, text_shadow_colour_state_str)
def set_image_clip(self, rect: Union[pygame.Rect, None]): """ Sets a clipping rectangle on this element's image determining what portion of it will actually be displayed when this element is blitted to the screen. :param rect: A clipping rectangle, or None to clear the clip. """ if rect is not None: if rect.width < 0: rect.width = 0 if rect.height < 0: rect.height = 0 if self._pre_clipped_image is None and self.image is not None: self._pre_clipped_image = self.image.copy() self._image_clip = rect if self.image is not None: self.image.fill(pygame.Color('#00000000')) basic_blit(self.image, self._pre_clipped_image, self._image_clip, self._image_clip) elif self._image_clip is not None: self._image_clip = None self.set_image(self._pre_clipped_image) else: self._image_clip = None
def _find_spot_in_lt_cache(self, cache_surface, new_item, string_id): """ Find a place in a long term cache surface for our new item from the short term cache. :param cache_surface: the surface to search. :param new_item: the item to cache. :param string_id: the look up id. :return: A tuple of the new rect we are reserving, and the rectangle it's inside of. """ found_rectangle_cache = None found_rectangle_to_split = None current_surface = cache_surface['surface'] surface_size = new_item[0].get_size() for free_rectangle in cache_surface['free_space_rectangles']: if (free_rectangle.width >= surface_size[0] and free_rectangle.height >= surface_size[1]): # we fits, so we sits found_rectangle_to_split = free_rectangle found_rectangle_cache = pygame.Rect(free_rectangle.topleft, surface_size) basic_blit(current_surface, new_item[0], free_rectangle.topleft) self.cache_long_term_lookup[string_id] = { 'surface': current_surface.subsurface(found_rectangle_cache), 'current_uses': new_item[1], 'total_uses': new_item[1] } break return found_rectangle_cache, found_rectangle_to_split
def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Changes the size of the rounded rectangle shape. Relatively expensive to completely do so has support for 'temporary rapid resizing' while the dimensions are being changed frequently. :param dimensions: The new dimensions. """ if (dimensions[0] == self.containing_rect.width and dimensions[1] == self.containing_rect.height): return False self.containing_rect.width = int(dimensions[0]) self.containing_rect.height = int(dimensions[1]) self.click_area_shape.width = int( dimensions[0]) - (2 * self.shadow_width) self.click_area_shape.height = int( dimensions[1]) - (2 * self.shadow_width) if self.shadow_width > 0: quick_surf = self.ui_manager.get_shadow( self.containing_rect.size, self.shadow_width, 'rectangle', corner_radius=self.shadow_width + 2) else: quick_surf = pygame.surface.Surface(self.containing_rect.size, flags=pygame.SRCALPHA, depth=32) quick_surf.fill(pygame.Color('#00000000')) if isinstance(self.theming['normal_bg'], ColourGradient): grad_surf = pygame.surface.Surface(self.click_area_shape.size, flags=pygame.SRCALPHA, depth=32) grad_surf.fill(pygame.Color('#FFFFFFFF')) self.theming['normal_bg'].apply_gradient_to_surface(grad_surf) basic_blit( quick_surf, grad_surf, pygame.Rect((self.shadow_width, self.shadow_width), self.click_area_shape.size)) else: quick_surf.fill( self.theming['normal_bg'], pygame.Rect((self.shadow_width, self.shadow_width), self.click_area_shape.size)) self.states['normal'].surface = quick_surf self.finalise_images_and_text('normal_image', 'normal', 'normal_text', 'normal_text_shadow', True) self.states['normal'].has_fresh_surface = True self.has_been_resized = True self.should_trigger_full_rebuild = True self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size return True
def blit_finalised_text_to_surf(self, surface: Surface): """ Lets us blit a finalised text surface to an arbitrary surface. Useful for doing stuff with text effects. :param surface: the target surface to blit onto. """ if self.finalised_surface is not None: basic_blit(surface, self.finalised_surface, (0, 0))
def apply_active_text_changes(self): """ Updates the shape surface with any changes to the text surface. Useful when we've made small edits to the text surface """ if self.text_box_layout is not None: for state_id, state in self.states.items(): if state.pre_text_surface is not None and state.text_surface is not None: state.surface = state.pre_text_surface.copy() basic_blit(state.surface, state.text_surface, (0, 0))
def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ Method to directly set the dimensions of a text box. :param dimensions: The new dimensions to set. """ self.relative_rect.width = int(dimensions[0]) self.relative_rect.height = int(dimensions[1]) self.rect.size = self.relative_rect.size if dimensions[0] >= 0 and dimensions[1] >= 0: if self.relative_right_margin is not None: self.relative_right_margin = self.ui_container.rect.right - self.rect.right if self.relative_bottom_margin is not None: self.relative_bottom_margin = self.ui_container.rect.bottom - self.rect.bottom self._update_container_clip() # Quick and dirty temporary scaling to cut down on number of # full rebuilds triggered when rapid scaling if self.image is not None: if (self.full_rebuild_countdown > 0.0 and (self.relative_rect.width > 0 and self.relative_rect.height > 0)): new_image = pygame.surface.Surface(self.relative_rect.size, flags=pygame.SRCALPHA, depth=32) new_image.fill(pygame.Color('#00000000')) basic_blit(new_image, self.image, (0, 0)) self.set_image(new_image) if self.scroll_bar is not None: self.scroll_bar.set_dimensions( (self.scroll_bar.relative_rect.width, self.relative_rect.height - (2 * self.border_width) - (2 * self.shadow_width))) scroll_bar_position = (self.relative_rect.right - self.border_width - self.shadow_width - self.scroll_bar_width, self.relative_rect.top + self.border_width + self.shadow_width) self.scroll_bar.set_relative_position( scroll_bar_position) self.should_trigger_full_rebuild = True self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size
def _rebuild_shadow(self, new_image, text_render_rect): shadow_text_render = render_white_text_alpha_black_bg(self.font, self.text) apply_colour_to_surface(self.text_shadow_colour, shadow_text_render) for y_pos in range(-self.text_shadow_size, self.text_shadow_size + 1): shadow_text_rect = pygame.Rect((text_render_rect.x + self.text_shadow_offset[0], text_render_rect.y + self.text_shadow_offset[1] + y_pos), text_render_rect.size) basic_blit(new_image, shadow_text_render, shadow_text_rect) for x_pos in range(-self.text_shadow_size, self.text_shadow_size + 1): shadow_text_rect = pygame.Rect((text_render_rect.x + self.text_shadow_offset[0] + x_pos, text_render_rect.y + self.text_shadow_offset[1]), text_render_rect.size) basic_blit(new_image, shadow_text_render, shadow_text_rect) for x_and_y in range(-self.text_shadow_size, self.text_shadow_size + 1): shadow_text_rect = pygame.Rect( (text_render_rect.x + self.text_shadow_offset[0] + x_and_y, text_render_rect.y + self.text_shadow_offset[1] + x_and_y), text_render_rect.size) basic_blit(new_image, shadow_text_render, shadow_text_rect) for x_and_y in range(-self.text_shadow_size, self.text_shadow_size + 1): shadow_text_rect = pygame.Rect( (text_render_rect.x + self.text_shadow_offset[0] - x_and_y, text_render_rect.y + self.text_shadow_offset[1] + x_and_y), text_render_rect.size) basic_blit(new_image, shadow_text_render, shadow_text_rect)
def redraw(self): """ Redraws the entire text entry element onto the underlying sprite image. Usually called when the displayed text has been edited or changed in some fashion. """ if self.is_enabled: if isinstance(self.background_colour, ColourGradient): self.text_image.fill(pygame.Color("#FFFFFFFF")) self.background_colour.apply_gradient_to_surface( self.text_image) else: self.text_image.fill(self.background_colour) else: if isinstance(self.disabled_background_colour, ColourGradient): self.text_image.fill(pygame.Color("#FFFFFFFF")) self.disabled_background_colour.apply_gradient_to_surface( self.text_image) else: self.text_image.fill(self.disabled_background_colour) if self.select_range[0] == self.select_range[1]: self._redraw_unselected_text() else: self._redraw_selected_text() text_clip_width = (self.rect.width - (self.padding[0] * 2) - (self.shape_corner_radius * 2) - (self.border_width * 2) - (self.shadow_width * 2)) width_to_edit_pos = self.font.size(self.text[:self.edit_position])[0] if self.start_text_offset > width_to_edit_pos: self.start_text_offset = width_to_edit_pos elif width_to_edit_pos > (self.start_text_offset + text_clip_width): self.start_text_offset = max(0, width_to_edit_pos - text_clip_width) elif width_to_edit_pos == 0: self.start_text_offset = 0 if len(self.text) > 0: basic_blit( self.text_image, self.text_surface, self.padding, pygame.Rect( (self.start_text_offset, 0), (text_clip_width, (self.rect.height - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2))))) self.redraw_cursor()
def set_visual_debug_mode(self, activate_mode: bool): """ Enables a debug mode for the element which displays layer information on top of it in a tiny font. :param activate_mode: True or False to enable or disable the mode. """ if activate_mode: default_font = self.ui_manager.get_theme().get_font_dictionary( ).get_default_font() layer_text_render = render_white_text_alpha_black_bg( default_font, "UI Layer: " + str(self._layer)) if self.image is not None: self.pre_debug_image = self.image.copy() # check if our surface is big enough to hold the debug info, # if not make a new, bigger copy make_new_larger_surface = False surf_width = self.image.get_width() surf_height = self.image.get_height() if self.image.get_width() < layer_text_render.get_width(): make_new_larger_surface = True surf_width = layer_text_render.get_width() if self.image.get_height() < layer_text_render.get_height(): make_new_larger_surface = True surf_height = layer_text_render.get_height() if make_new_larger_surface: new_surface = pygame.surface.Surface( (surf_width, surf_height), flags=pygame.SRCALPHA, depth=32) basic_blit(new_surface, self.image, (0, 0)) self.set_image(new_surface) basic_blit(self.image, layer_text_render, (0, 0)) else: self.set_image(layer_text_render) self._visual_debug_mode = True else: self.rebuild() self._visual_debug_mode = False
def finalise_text(self, state_str, text_colour_state_str: str = "", text_shadow_colour_state_str: str = "", only_text_changed: bool = False): """ Finalise the text to a surface with some last-minute data that doesn't require the text be re-laid out. :param only_text_changed: :param state_str: The name of the shape's state we are finalising. :param text_colour_state_str: The string identifying the text colour to use. :param text_shadow_colour_state_str: The string identifying the text shadow colour to use. """ if self.text_box_layout is not None: # copy the pre-text surface & create a new empty text surface for this state self.states[state_str].pre_text_surface = self.states[ state_str].surface.copy() self.states[state_str].text_surface = pygame.surface.Surface( self.states[state_str].surface.get_size(), flags=pygame.SRCALPHA, depth=32) self.states[state_str].text_surface.fill('#00000000') if only_text_changed: self.text_box_layout.blit_finalised_text_to_surf( self.states[state_str].text_surface) basic_blit(self.states[state_str].surface, self.states[state_str].text_surface, (0, 0)) else: self.text_box_layout.set_default_text_colour( self.theming[text_colour_state_str]) self.text_box_layout.set_default_text_shadow_colour( self.theming[text_shadow_colour_state_str]) self.text_box_layout.finalise_to_surf( self.states[state_str].text_surface) basic_blit(self.states[state_str].surface, self.states[state_str].text_surface, (0, 0))
def redraw_cursor(self): """ Redraws only the blinking edit cursor. This allows us to blink the cursor on and off without spending time redrawing all the text. """ new_image = self.background_and_border.copy() basic_blit(new_image, self.text_image, self.text_image_rect) if self.cursor_on and self.is_enabled: cursor_len_str = self.text[:self.edit_position] cursor_size = self.font.size(cursor_len_str) self.cursor.x = (cursor_size[0] + self.text_image_rect.x + self.padding[0] - self.start_text_offset) if not isinstance(self.text_colour, ColourGradient): pygame.draw.rect(new_image, self.text_colour, self.cursor) else: cursor_surface = pygame.surface.Surface(self.cursor.size, flags=pygame.SRCALPHA, depth=32) cursor_surface.fill(pygame.Color('#FFFFFFFF')) self.text_colour.apply_gradient_to_surface(cursor_surface) basic_blit(new_image, cursor_surface, self.cursor) self.set_image(new_image)
def update(self, time_delta: float): """ Called once every update loop of the UI Manager. Used to react to scroll bar movement (if there is one), update the text effect (if there is one) and check if we are hovering over any text links (if there are any). :param time_delta: The time in seconds between calls to update. Useful for timing things. """ super().update(time_delta) if not self.alive(): return if self.scroll_bar is not None and self.scroll_bar.check_has_moved_recently( ): height_adjustment = int(self.scroll_bar.start_percentage * self.text_box_layout.layout_rect.height) drawable_area_size = (max( 1, (self.rect[2] - (self.padding[0] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset))), max(1, (self.rect[3] - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) drawable_area = pygame.Rect((0, height_adjustment), drawable_area_size) if self.rect.width <= 0 or self.rect.height <= 0: return new_image = pygame.surface.Surface(self.rect.size, flags=pygame.SRCALPHA, depth=32) new_image.fill(pygame.Color(0, 0, 0, 0)) basic_blit(new_image, self.background_surf, (0, 0)) basic_blit(new_image, self.text_box_layout.finalised_surface, (self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset, self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset), drawable_area) self.set_image(new_image) mouse_x, mouse_y = self.ui_manager.get_mouse_position() should_redraw_from_layout = False if self.scroll_bar is not None: height_adjustment = (self.scroll_bar.start_percentage * self.text_box_layout.layout_rect.height) else: height_adjustment = 0 base_x = int(self.rect[0] + self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset) base_y = int(self.rect[1] + self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset - height_adjustment) for chunk in self.link_hover_chunks: hovered_currently = False hover_rect = pygame.Rect((base_x + chunk.x, base_y + chunk.y), chunk.size) if hover_rect.collidepoint(mouse_x, mouse_y) and self.rect.collidepoint( mouse_x, mouse_y): hovered_currently = True if chunk.is_hovered and not hovered_currently: chunk.on_unhovered() should_redraw_from_layout = True elif hovered_currently and not chunk.is_hovered: chunk.on_hovered() should_redraw_from_layout = True if should_redraw_from_layout: self.redraw_from_text_block() self.update_text_effect(time_delta) if self.should_trigger_full_rebuild and self.full_rebuild_countdown <= 0.0: self.rebuild() if self.full_rebuild_countdown > 0.0: self.full_rebuild_countdown -= time_delta
def rebuild(self): """ Rebuild whatever needs building. """ if self.scroll_bar is not None: self.scroll_bar.kill() # The text_wrap_area is the part of the text box that we try to keep the text inside # of so that none of it overlaps. Essentially we start with the containing box, # subtract the border, then subtract the padding, then if necessary subtract the width # of the scroll bar self.rounded_corner_offset = int(self.shape_corner_radius - (math.sin(math.pi / 4) * self.shape_corner_radius)) self.text_wrap_rect = pygame.Rect( (self.rect[0] + self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset), (self.rect[1] + self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset), max(1, (self.rect[2] - (self.padding[0] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset))), max(1, (self.rect[3] - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) if self.wrap_to_height or self.rect[3] == -1: self.text_wrap_rect.height = -1 if self.rect[2] == -1: self.text_wrap_rect.width = -1 drawable_area_size = (self.text_wrap_rect[2], self.text_wrap_rect[3]) # This gives us the height of the text at the 'width' of the text_wrap_area self.parse_html_into_style_data() if self.text_box_layout is not None: if self.wrap_to_height or self.rect[3] == -1 or self.rect[2] == -1: final_text_area_size = self.text_box_layout.layout_rect.size new_dimensions = ( (final_text_area_size[0] + (self.padding[0] * 2) + (self.border_width * 2) + (self.shadow_width * 2) + (2 * self.rounded_corner_offset)), (final_text_area_size[1] + (self.padding[1] * 2) + (self.border_width * 2) + (self.shadow_width * 2) + (2 * self.rounded_corner_offset))) self.set_dimensions(new_dimensions) # need to regen this because it was dynamically generated drawable_area_size = (max( 1, (self.rect[2] - (self.padding[0] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset))), max(1, (self.rect[3] - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) elif self.text_box_layout.layout_rect.height > self.text_wrap_rect[ 3]: # We need a scrollbar because our text is longer than the space we # have to display it. This also means we need to parse the text again. text_rect_width = (self.rect[2] - (self.padding[0] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - self.rounded_corner_offset - self.scroll_bar_width) self.text_wrap_rect = pygame.Rect( (self.rect[0] + self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset), (self.rect[1] + self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset), max(1, text_rect_width), max(1, (self.rect[3] - (self.padding[1] * 2) - (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) self.parse_html_into_style_data() percentage_visible = (self.text_wrap_rect[3] / self.text_box_layout.layout_rect.height) scroll_bar_position = (self.relative_rect.right - self.border_width - self.shadow_width - self.scroll_bar_width, self.relative_rect.top + self.border_width + self.shadow_width) scroll_bar_rect = pygame.Rect( scroll_bar_position, (self.scroll_bar_width, self.rect.height - (2 * self.border_width) - (2 * self.shadow_width))) self.scroll_bar = UIVerticalScrollBar(scroll_bar_rect, percentage_visible, self.ui_manager, self.ui_container, parent_element=self, visible=self.visible) self.join_focus_sets(self.scroll_bar) else: new_dimensions = (self.rect[2], self.rect[3]) self.set_dimensions(new_dimensions) theming_parameters = { 'normal_bg': self.background_colour, 'normal_border': self.border_colour, 'border_width': self.border_width, 'shadow_width': self.shadow_width, 'shape_corner_radius': self.shape_corner_radius } if self.shape == 'rectangle': self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, ['normal'], self.ui_manager) elif self.shape == 'rounded_rectangle': self.drawable_shape = RoundedRectangleShape( self.rect, theming_parameters, ['normal'], self.ui_manager) self.background_surf = self.drawable_shape.get_fresh_surface() if self.scroll_bar is not None: height_adjustment = int(self.scroll_bar.start_percentage * self.text_box_layout.layout_rect.height) else: height_adjustment = 0 if self.rect.width <= 0 or self.rect.height <= 0: return drawable_area = pygame.Rect((0, height_adjustment), drawable_area_size) new_image = pygame.surface.Surface(self.rect.size, flags=pygame.SRCALPHA, depth=32) new_image.fill(pygame.Color(0, 0, 0, 0)) basic_blit(new_image, self.background_surf, (0, 0)) basic_blit( new_image, self.text_box_layout.finalised_surface, (self.padding[0] + self.border_width + self.shadow_width + self.rounded_corner_offset, self.padding[1] + self.border_width + self.shadow_width + self.rounded_corner_offset), drawable_area) self.set_image(new_image) self.link_hover_chunks = [] self.text_box_layout.add_chunks_to_hover_group(self.link_hover_chunks) self.should_trigger_full_rebuild = False self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size
def rebuild(self): """ Re-render the text to the label's underlying sprite image. This allows us to change what the displayed text is or remake it with different theming (if the theming has changed). """ text_size = self.font.size(self.text) if text_size[1] > self.relative_rect.height or text_size[0] > self.relative_rect.width: width_overlap = self.relative_rect.width - text_size[0] height_overlap = self.relative_rect.height - text_size[1] warn_text = ('Label Rect is too small for text: ' '' + self.text + ' - size diff: ' + str((width_overlap, height_overlap))) warnings.warn(warn_text, UserWarning) new_image = pygame.surface.Surface(self.relative_rect.size, flags=pygame.SRCALPHA, depth=32) if isinstance(self.bg_colour, ColourGradient): new_image.fill(pygame.Color('#FFFFFFFF')) self.bg_colour.apply_gradient_to_surface(new_image) text_render = render_white_text_alpha_black_bg(self.font, self.text) if self.is_enabled: if isinstance(self.text_colour, ColourGradient): self.text_colour.apply_gradient_to_surface(text_render) else: apply_colour_to_surface(self.text_colour, text_render) else: if isinstance(self.disabled_text_colour, ColourGradient): self.disabled_text_colour.apply_gradient_to_surface(text_render) else: apply_colour_to_surface(self.disabled_text_colour, text_render) else: new_image.fill(self.bg_colour) if self.is_enabled: if isinstance(self.text_colour, ColourGradient): text_render = render_white_text_alpha_black_bg(self.font, self.text) self.text_colour.apply_gradient_to_surface(text_render) else: if self.bg_colour.a != 255 or self.text_shadow: text_render = render_white_text_alpha_black_bg(self.font, self.text) apply_colour_to_surface(self.text_colour, text_render) else: text_render = self.font.render(self.text, True, self.text_colour, self.bg_colour) text_render = text_render.convert_alpha() else: if isinstance(self.disabled_text_colour, ColourGradient): text_render = render_white_text_alpha_black_bg(self.font, self.text) self.disabled_text_colour.apply_gradient_to_surface(text_render) else: if self.bg_colour.a != 255 or self.text_shadow: text_render = render_white_text_alpha_black_bg(self.font, self.text) apply_colour_to_surface(self.disabled_text_colour, text_render) else: text_render = self.font.render(self.text, True, self.disabled_text_colour, self.bg_colour) text_render = text_render.convert_alpha() text_render_rect = text_render.get_rect(centerx=int(self.rect.width / 2), centery=int(self.rect.height / 2)) if self.text_shadow: self._rebuild_shadow(new_image, text_render_rect) basic_blit(new_image, text_render, text_render_rect) self.set_image(new_image)
def _draw_chunks_to_surface( self, lines_of_chunks: List[Dict[str, Union[List[Dict[str, Any]], int]]]): """ Takes a list of lines of chunks and draws it to a surface using the styles and positions attached to the chunks. :param lines_of_chunks: :return: """ self.block_sprite = None if self.height != -1 and self.width != -1: self.block_sprite = pygame.surface.Surface( (self.width, self.height), pygame.SRCALPHA, depth=32) self.block_sprite.fill(pygame.Color('#00000000')) position = [0, 0] line_height_acc = 0 max_line_length = 0 for line in lines_of_chunks: line_chunks = [] max_line_char_height = 0 for chunk in line['chunks']: if len(chunk['text']) > 0: new_chunk = StyledChunk( chunk['style'].font_size, chunk['style'].font_name, chunk['text'], chunk['style'].style, chunk['style'].colour, chunk['style'].bg_colour, chunk['style'].is_link, chunk['style'].link_href, self.link_style, (position[0], position[1]), self.font_dict) position[0] += new_chunk.advance if new_chunk.height > max_line_char_height: max_line_char_height = new_chunk.height line_chunks.append(new_chunk) if self.block_sprite is not None: # need to adjust y start pos based on ascents new_chunk.rect.y += (line['line_ascent'] - new_chunk.ascent) basic_blit(self.block_sprite, new_chunk.rendered_chunk, new_chunk.rect) text_line = TextBlock.TextLine() text_line.chunks = line_chunks text_line.max_line_ascent = line['line_ascent'] self.lines.append(text_line) if position[0] > max_line_length: max_line_length = position[0] position[0] = 0 position[1] += max_line_char_height line_height_acc += max_line_char_height if self.block_sprite is None: self.width = max_line_length if self.width == -1 else self.width self.height = line_height_acc if self.height == -1 else self.height self.block_sprite = pygame.surface.Surface( (self.width, self.height), pygame.SRCALPHA, depth=32) self.block_sprite.fill(pygame.Color('#00000000')) for line in self.lines: for chunk in line.chunks: # need to adjust y start pos based on ascents chunk.rect.y += line.max_line_ascent - chunk.ascent basic_blit(self.block_sprite, chunk.rendered_chunk, chunk.rect) self.final_dimensions = (self.width, self.height)
def rebuild_images_and_text(self, image_state_str: str, state_str: str, text_colour_state_str: str): """ Rebuilds any text or image used by a specific state in the drawable shape. Effectively this means adding them on top of whatever is already in the state's surface. As such it should generally be called last in the process of building up a finished drawable shape state. :param image_state_str: image ID of the state we are going to be adding images and text to. :param state_str: normal ID of the state we are going to be adding images and text to. :param text_colour_state_str: text ID of the state we are going to be adding images and text to. """ # Draw any themed images if image_state_str in self.theming and self.theming[ image_state_str] is not None: image_rect = self.theming[image_state_str].get_rect() image_rect.center = (int(self.containing_rect.width / 2), int(self.containing_rect.height / 2)) basic_blit(self.states[state_str].surface, self.theming[image_state_str], image_rect) # Draw any text if 'text' in self.theming and 'font' in self.theming and self.theming[ 'text'] is not None: if len(self.theming['text'] ) > 0 and text_colour_state_str in self.theming: text_surface = render_white_text_alpha_black_bg( font=self.theming['font'], text=self.theming['text']) if isinstance(self.theming[text_colour_state_str], ColourGradient): self.theming[ text_colour_state_str].apply_gradient_to_surface( text_surface) else: apply_colour_to_surface( self.theming[text_colour_state_str], text_surface) else: text_surface = None if 'text_shadow' in self.theming: text_shadow = render_white_text_alpha_black_bg( font=self.theming['font'], text=self.theming['text']) apply_colour_to_surface(self.theming['text_shadow'], text_shadow) basic_blit( self.states[state_str].surface, text_shadow, (self.aligned_text_rect.x, self.aligned_text_rect.y + 1)) basic_blit( self.states[state_str].surface, text_shadow, (self.aligned_text_rect.x, self.aligned_text_rect.y - 1)) basic_blit( self.states[state_str].surface, text_shadow, (self.aligned_text_rect.x + 1, self.aligned_text_rect.y)) basic_blit( self.states[state_str].surface, text_shadow, (self.aligned_text_rect.x - 1, self.aligned_text_rect.y)) if text_surface is not None and self.aligned_text_rect is not None: basic_blit(self.states[state_str].surface, text_surface, self.aligned_text_rect)
def redraw_state(self, state_str: str): """ Redraws the shape's surface for a given UI state. :param state_str: The ID string of the state to rebuild. """ text_colour_state_str = state_str + '_text' image_state_str = state_str + '_image' bg_col = self.theming[state_str + '_bg'] border_col = self.theming[state_str + '_border'] found_shape = None shape_id = None if 'filled_bar' not in self.theming and 'filled_bar_width_percentage' not in self.theming: shape_id = self.shape_cache.build_cache_id('rounded_rectangle', self.containing_rect.size, self.shadow_width, self.border_width, border_col, bg_col, self.corner_radius) found_shape = self.shape_cache.find_surface_in_cache(shape_id) if found_shape is not None: self.states[state_str].surface = found_shape.copy() else: border_corner_radius = self.corner_radius self.states[state_str].surface = self.base_surface.copy() # Try one AA call method aa_amount = 4 self.border_rect = pygame.Rect((self.shadow_width * aa_amount, self.shadow_width * aa_amount), (self.click_area_shape.width * aa_amount, self.click_area_shape.height * aa_amount)) self.background_rect = pygame.Rect(((self.border_width + self.shadow_width) * aa_amount, (self.border_width + self.shadow_width) * aa_amount), (self.border_rect.width - (2 * self.border_width * aa_amount), self.border_rect.height - (2 * self.border_width * aa_amount))) dimension_scale = min(self.background_rect.width / max(self.border_rect.width, 1), self.background_rect.height / max(self.border_rect.height, 1)) bg_corner_radius = int(border_corner_radius * dimension_scale) bab_surface = pygame.surface.Surface((self.containing_rect.width * aa_amount, self.containing_rect.height * aa_amount), flags=pygame.SRCALPHA, depth=32) bab_surface.fill(pygame.Color('#00000000')) if self.border_width > 0: shape_surface = self.clear_and_create_shape_surface(bab_surface, self.border_rect, 0, border_corner_radius, aa_amount=aa_amount, clear=False) if isinstance(border_col, ColourGradient): border_col.apply_gradient_to_surface(shape_surface) else: apply_colour_to_surface(border_col, shape_surface) basic_blit(bab_surface, shape_surface, self.border_rect) shape_surface = self.clear_and_create_shape_surface(bab_surface, self.background_rect, 0, bg_corner_radius, aa_amount=aa_amount) if 'filled_bar' in self.theming and 'filled_bar_width_percentage' in self.theming: self._redraw_filled_bar(bg_col, shape_surface) else: if isinstance(bg_col, ColourGradient): bg_col.apply_gradient_to_surface(shape_surface) else: apply_colour_to_surface(bg_col, shape_surface) basic_blit(bab_surface, shape_surface, self.background_rect) # apply AA to background bab_surface = pygame.transform.smoothscale(bab_surface, self.containing_rect.size) basic_blit(self.states[state_str].surface, bab_surface, (0, 0)) if self.states[state_str].cached_background_id is not None: cached_id = self.states[state_str].cached_background_id self.shape_cache.remove_user_from_cache_item(cached_id) if (not self.has_been_resized and ((self.containing_rect.width * self.containing_rect.height) < 40000) and (shape_id is not None and self.states[state_str].surface.get_width() <= 1024 and self.states[state_str].surface.get_height() <= 1024)): self.shape_cache.add_surface_to_cache(self.states[state_str].surface.copy(), shape_id) self.states[state_str].cached_background_id = shape_id self.rebuild_images_and_text(image_state_str, state_str, text_colour_state_str) self.states[state_str].has_fresh_surface = True self.states[state_str].generated = True
def redraw_state(self, state_str: str): """ Redraws the shape's surface for a given UI state. :param state_str: The ID string of the state to rebuild. """ border_colour_state_str = state_str + '_border' bg_colour_state_str = state_str + '_bg' text_colour_state_str = state_str + '_text' image_state_str = state_str + '_image' found_shape = None shape_id = None if 'filled_bar' not in self.theming and 'filled_bar_width_percentage' not in self.theming: shape_id = self.shape_cache.build_cache_id( 'ellipse', self.containing_rect.size, self.shadow_width, self.border_width, self.theming[border_colour_state_str], self.theming[bg_colour_state_str]) found_shape = self.shape_cache.find_surface_in_cache(shape_id) if found_shape is not None: self.states[state_str].surface = found_shape.copy() else: self.states[state_str].surface = self.base_surface.copy() # Try one AA call method aa_amount = 4 self.border_rect = pygame.Rect( (self.shadow_width * aa_amount, self.shadow_width * aa_amount), (self.click_area_shape.width * aa_amount, self.click_area_shape.height * aa_amount)) self.background_rect = pygame.Rect( ((self.border_width + self.shadow_width) * aa_amount, (self.border_width + self.shadow_width) * aa_amount), (self.border_rect.width - (2 * self.border_width * aa_amount), self.border_rect.height - (2 * self.border_width * aa_amount))) bab_surface = pygame.surface.Surface( (self.containing_rect.width * aa_amount, self.containing_rect.height * aa_amount), flags=pygame.SRCALPHA, depth=32) bab_surface.fill(pygame.Color('#00000000')) if self.border_width > 0: if isinstance(self.theming[border_colour_state_str], ColourGradient): shape_surface = self.clear_and_create_shape_surface( bab_surface, self.border_rect, 0, aa_amount=aa_amount, clear=False) self.theming[ border_colour_state_str].apply_gradient_to_surface( shape_surface) else: shape_surface = self.clear_and_create_shape_surface( bab_surface, self.border_rect, 0, aa_amount=aa_amount, clear=False) apply_colour_to_surface( self.theming[border_colour_state_str], shape_surface) basic_blit(bab_surface, shape_surface, self.border_rect) if isinstance(self.theming[bg_colour_state_str], ColourGradient): shape_surface = self.clear_and_create_shape_surface( bab_surface, self.background_rect, 1, aa_amount=aa_amount) self.theming[bg_colour_state_str].apply_gradient_to_surface( shape_surface) else: shape_surface = self.clear_and_create_shape_surface( bab_surface, self.background_rect, 1, aa_amount=aa_amount) apply_colour_to_surface(self.theming[bg_colour_state_str], shape_surface) basic_blit(bab_surface, shape_surface, self.background_rect) # apply AA to background bab_surface = pygame.transform.smoothscale( bab_surface, self.containing_rect.size) # cut a hole in shadow, then blit background into it sub_surface = pygame.surface.Surface( ((self.containing_rect.width - (2 * self.shadow_width)) * aa_amount, (self.containing_rect.height - (2 * self.shadow_width)) * aa_amount), flags=pygame.SRCALPHA, depth=32) sub_surface.fill(pygame.Color('#00000000')) pygame.draw.ellipse(sub_surface, pygame.Color("#FFFFFFFF"), sub_surface.get_rect()) small_sub = pygame.transform.smoothscale( sub_surface, (self.containing_rect.width - (2 * self.shadow_width), self.containing_rect.height - (2 * self.shadow_width))) self.states[state_str].surface.blit( small_sub, pygame.Rect((self.shadow_width, self.shadow_width), sub_surface.get_size()), special_flags=pygame.BLEND_RGBA_SUB) basic_blit(self.states[state_str].surface, bab_surface, (0, 0)) if (shape_id is not None and self.states[state_str].surface.get_width() <= 1024 and self.states[state_str].surface.get_height() <= 1024): self.shape_cache.add_surface_to_cache( self.states[state_str].surface.copy(), shape_id) self.rebuild_images_and_text(image_state_str, state_str, text_colour_state_str) self.states[state_str].has_fresh_surface = True self.states[state_str].generated = True
def redraw_state(self, state_str: str, add_text: bool = True): """ Redraws the shape's surface for a given UI state. :param add_text: :param state_str: The ID string of the state to rebuild. """ border_colour_state_str = state_str + '_border' bg_colour_state_str = state_str + '_bg' text_colour_state_str = state_str + '_text' text_shadow_colour_state_str = state_str + '_text_shadow' image_state_str = state_str + '_image' found_shape = None shape_id = None if 'filled_bar' not in self.theming and 'filled_bar_width_percentage' not in self.theming: shape_id = self.shape_cache.build_cache_id('rectangle', self.containing_rect.size, self.shadow_width, self.border_width, self.theming[border_colour_state_str], self.theming[bg_colour_state_str]) found_shape = self.shape_cache.find_surface_in_cache(shape_id) if found_shape is not None: self.states[state_str].surface = found_shape.copy() else: self.states[state_str].surface = self.base_surface.copy() if self.border_width > 0: if isinstance(self.theming[border_colour_state_str], ColourGradient): border_shape_surface = pygame.surface.Surface(self.border_rect.size, flags=pygame.SRCALPHA, depth=32) border_shape_surface.fill(pygame.Color('#FFFFFFFF')) self.states[state_str].surface.blit(border_shape_surface, self.border_rect, special_flags=pygame.BLEND_RGBA_SUB) self.theming[border_colour_state_str].apply_gradient_to_surface( border_shape_surface) basic_blit(self.states[state_str].surface, border_shape_surface, self.border_rect) else: self.states[state_str].surface.fill(self.theming[border_colour_state_str], self.border_rect) if isinstance(self.theming[bg_colour_state_str], ColourGradient): background_shape_surface = pygame.surface.Surface(self.background_rect.size, flags=pygame.SRCALPHA, depth=32) background_shape_surface.fill(pygame.Color('#FFFFFFFF')) self.states[state_str].surface.blit(background_shape_surface, self.background_rect, special_flags=pygame.BLEND_RGBA_SUB) self.theming[bg_colour_state_str].apply_gradient_to_surface( background_shape_surface) basic_blit(self.states[state_str].surface, background_shape_surface, self.background_rect) else: self.states[state_str].surface.fill(self.theming[bg_colour_state_str], self.background_rect) if 'filled_bar' in self.theming and 'filled_bar_width_percentage' in self.theming: bar_rect = pygame.Rect(self.background_rect.topleft, (int(self.theming['filled_bar_width_percentage'] * self.background_rect.width), self.background_rect.height)) if isinstance(self.theming['filled_bar'], ColourGradient): bar_shape_surface = pygame.surface.Surface(bar_rect.size, flags=pygame.SRCALPHA, depth=32) bar_shape_surface.fill(pygame.Color('#FFFFFFFF')) self.states[state_str].surface.blit(bar_shape_surface, bar_rect, special_flags=pygame.BLEND_RGBA_SUB) self.theming['filled_bar'].apply_gradient_to_surface(bar_shape_surface) basic_blit(self.states[state_str].surface, bar_shape_surface, bar_rect) else: self.states[state_str].surface.fill(self.theming['filled_bar'], bar_rect) if self.states[state_str].cached_background_id is not None: self.shape_cache.remove_user_from_cache_item( self.states[state_str].cached_background_id) if (not self.has_been_resized and ((self.containing_rect.width * self.containing_rect.height) < 40000) and (shape_id is not None and self.states[state_str].surface.get_width() <= 1024 and self.states[state_str].surface.get_height() <= 1024)): self.shape_cache.add_surface_to_cache(self.states[state_str].surface.copy(), shape_id) self.states[state_str].cached_background_id = shape_id self.finalise_images_and_text(image_state_str, state_str, text_colour_state_str, text_shadow_colour_state_str, add_text) self.states[state_str].has_fresh_surface = True self.states[state_str].generated = True
def _redraw_selected_text(self): """ Redraw text where some has been selected by a user. """ low_end = min(self.select_range[0], self.select_range[1]) high_end = max(self.select_range[0], self.select_range[1]) pre_select_area_text = self.text[:low_end] select_area_text = self.text[low_end:high_end] post_select_area_text = self.text[high_end:] pre_select_area_surface = None post_select_area_surface = None overall_size = self.font.size(self.text) advances = [ letter_metrics[4] for letter_metrics in self.font.metrics(self.text) ] pre_select_width = sum(advances[:low_end]) select_area_width = sum(advances[low_end:high_end]) if len(pre_select_area_text) > 0: pre_select_area_surface = self._draw_text_with_grad_or_col( pre_select_area_text, self.text_colour) if isinstance(self.selected_bg_colour, ColourGradient): select_area_surface = pygame.surface.Surface( (select_area_width, overall_size[1]), flags=pygame.SRCALPHA, depth=32) select_area_surface.fill(pygame.Color('#FFFFFFFF')) self.selected_bg_colour.apply_gradient_to_surface( select_area_surface) alpha_text = self._draw_text_with_grad_or_col( select_area_text, self.selected_text_colour) basic_blit(select_area_surface, alpha_text, (0, 0)) else: if isinstance(self.selected_text_colour, ColourGradient): select_area_surface = pygame.surface.Surface( (select_area_width, overall_size[1]), flags=pygame.SRCALPHA, depth=32) select_area_surface.fill(self.selected_bg_colour) alpha_text = render_white_text_alpha_black_bg( font=self.font, text=select_area_text) self.selected_text_colour.apply_gradient_to_surface(alpha_text) basic_blit(select_area_surface, alpha_text, (0, 0)) else: select_area_surface = self.font.render( select_area_text, True, self.selected_text_colour, self.selected_bg_colour).convert_alpha() if len(post_select_area_text) > 0: post_select_area_surface = self._draw_text_with_grad_or_col( post_select_area_text, self.text_colour) self.text_surface = pygame.surface.Surface(overall_size, flags=pygame.SRCALPHA, depth=32) if isinstance(self.background_colour, ColourGradient): self.text_image.fill(pygame.Color("#FFFFFFFF")) self.background_colour.apply_gradient_to_surface(self.text_image) else: self.text_image.fill(self.background_colour) if pre_select_area_surface is not None: basic_blit(self.text_surface, pre_select_area_surface, (0, 0)) basic_blit(self.text_surface, select_area_surface, (pre_select_width, 0)) if post_select_area_surface is not None: basic_blit(self.text_surface, post_select_area_surface, (pre_select_width + select_area_width, 0))