def test_horiz_center_row(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) simple_rect = SimpleTestLayoutRect(dimensions=(20, 30)) simple_rect_2 = SimpleTestLayoutRect(dimensions=(50, 30)) simple_rect_3 = SimpleTestLayoutRect(dimensions=(30, 30)) layout_row.add_item(simple_rect) layout_row.add_item(simple_rect_2) layout_row.add_item(simple_rect_3) layout_row.horiz_center_row(text_box_layout.floating_rects) assert layout_row.left == 50 assert layout_row.items[0].x == 50 assert layout_row.items[1].x == 70 assert layout_row.items[2].x == 120 assert layout_row.items[2].right == 150 assert layout_row.right == 150
def test_insert_text(self): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) font_1 = pygame.freetype.Font(None, 30) font_1.origin = True font_1.pad = True text_chunk_1 = TextLineChunkFTFont(text='test', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#00000000')) layout_row.add_item(text_chunk_1) layout_row.insert_text(' this', 4) assert layout_row.items[0].text == 'test this'
def test_rewind_row(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) simple_rect = SimpleTestLayoutRect(dimensions=(200, 30)) simple_rect_2 = SimpleTestLayoutRect(dimensions=(100, 30)) simple_rect_3 = SimpleTestLayoutRect(dimensions=(80, 30)) layout_row.add_item(simple_rect) layout_row.add_item(simple_rect_2) layout_row.add_item(simple_rect_3) rewound_data = deque([]) layout_row.rewind_row(rewound_data) assert rewound_data.pop().width == simple_rect_3.width assert rewound_data.pop().width == simple_rect_2.width assert rewound_data.pop().width == simple_rect.width
def test_clear(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) font_1 = pygame.freetype.Font(None, 30) font_1.origin = True font_1.pad = True text_chunk_1 = TextLineChunkFTFont(text='D', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#00000000')) layout_row.add_item(text_chunk_1) layout_surface = pygame.Surface((200, 300), depth=32, flags=pygame.SRCALPHA) layout_surface.fill((0, 0, 0, 0)) layout_row.finalise(layout_surface) assert layout_surface.get_at((18, 18)) == pygame.Color('#FFFFFF') layout_row.clear() assert layout_surface.get_at((18, 18)) == pygame.Color('#00000000')
def test_vert_align_items_to_row(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) font_1 = pygame.freetype.Font(None, 30) font_2 = pygame.freetype.Font(None, 20) font_3 = pygame.freetype.Font(None, 10) text_chunk_1 = TextLineChunkFTFont(text='hello ', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) text_chunk_2 = TextLineChunkFTFont(text='this is a', font=font_2, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) text_chunk_3 = TextLineChunkFTFont(text=' test', font=font_3, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) layout_row.add_item(text_chunk_1) layout_row.add_item(text_chunk_2) layout_row.add_item(text_chunk_3) # not sure this is right, need to do some more visual testing of vertical # alignment of text rects with different height text on a single row. assert layout_row.items[0].y == 0 assert layout_row.items[1].y == 7 assert layout_row.items[2].y == 15 layout_row.vert_align_items_to_row() assert layout_row.items[0].y == 0 assert layout_row.items[1].y == 7 assert layout_row.items[2].y == 15
def test_creation(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=1.0) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=1.25, layout=text_box_layout) assert layout_row.width == 0 assert layout_row.height == 0
def test_set_cursor_position(self): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) font_1 = pygame.freetype.Font(None, 30) font_1.origin = True font_1.pad = True text_chunk_1 = TextLineChunkFTFont(text='test', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#00000000')) layout_row.add_item(text_chunk_1) layout_surface = pygame.Surface((200, 300), depth=32, flags=pygame.SRCALPHA) layout_surface.fill((0, 0, 0, 0)) layout_row.finalise(layout_surface) layout_row.toggle_cursor() assert layout_row.edit_cursor_active is True assert layout_surface.get_at((1, 5)) == pygame.Color('#FFFFFF') assert layout_row.cursor_index == 0 assert layout_row.cursor_draw_width == 0 layout_row.set_cursor_position(3) layout_row.toggle_cursor() layout_row.toggle_cursor() assert layout_row.edit_cursor_active is True assert layout_surface.get_at((1, 5)) == pygame.Color('#00000000') assert layout_row.cursor_index == 3 assert layout_row.cursor_draw_width == 44
def test_at_start(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=1.0) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=1.25, layout=text_box_layout) assert layout_row.at_start() simple_rect = SimpleTestLayoutRect(dimensions=(200, 30)) layout_row.add_item(simple_rect) assert not layout_row.at_start()
def test_merge_adjacent_compatible_chunks(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) font_1 = pygame.freetype.Font(None, 30) font_2 = pygame.freetype.Font(None, 20) text_chunk_1 = TextLineChunkFTFont(text='hello ', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) text_chunk_2 = TextLineChunkFTFont(text='this is a', font=font_1, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) text_chunk_3 = TextLineChunkFTFont(text=' test', font=font_2, underlined=False, colour=pygame.Color('#FFFFFF'), using_default_text_colour=False, bg_colour=pygame.Color('#FF0000')) layout_row.add_item(text_chunk_1) layout_row.add_item(text_chunk_2) layout_row.add_item(text_chunk_3) assert len(layout_row.items) == 3 layout_row.merge_adjacent_compatible_chunks() assert len(layout_row.items) == 2
def test_add_item(self, _init_pygame, default_ui_manager: UIManager): input_data = deque([]) line_spacing = 1.25 text_box_layout = TextBoxLayout(input_data_queue=input_data, layout_rect=pygame.Rect( 0, 0, 200, 300), view_rect=pygame.Rect(0, 0, 200, 150), line_spacing=line_spacing) layout_row = TextBoxLayoutRow(row_start_x=0, row_start_y=0, row_index=0, line_spacing=line_spacing, layout=text_box_layout) simple_rect = SimpleTestLayoutRect(dimensions=(200, 30)) layout_row.add_item(simple_rect) assert len(layout_row.items) == 1 assert layout_row.width == simple_rect.width assert layout_row.text_chunk_height == simple_rect.height assert layout_row.height == int(simple_rect.height * line_spacing)
def build_text_layout(self): """ Build a text box layout for this drawable shape if it has some text. """ containing_rect_when_text_built = self.containing_rect.copy() # Draw any text if 'text' in self.theming and 'font' in self.theming and self.theming[ 'text'] is not None: # we need two rectangles for the text. One is has actual area the # text surface takes up, which may be larger than the displayed area, # and its position on the final surface. The other is the amount of # area of the text surface which we blit from, which may be much smaller # than the total text area. horiz_padding = 0 if 'text_horiz_alignment_padding' in self.theming: horiz_padding = self.theming['text_horiz_alignment_padding'] vert_padding = 0 if 'text_vert_alignment_padding' in self.theming: vert_padding = self.theming['text_vert_alignment_padding'] total_text_buffer = self.shadow_width + self.border_width + self.rounded_corner_offset self.text_view_rect = self.containing_rect.copy() self.text_view_rect.x = 0 self.text_view_rect.y = 0 if self.text_view_rect.width != -1: self.text_view_rect.width -= ((total_text_buffer * 2) + (2 * horiz_padding)) if self.text_view_rect.height != -1: self.text_view_rect.height -= ((total_text_buffer * 2) + (2 * vert_padding)) text_actual_area_rect = self.text_view_rect.copy() text_actual_area_rect.x = total_text_buffer + horiz_padding text_actual_area_rect.y = total_text_buffer + vert_padding if 'text_width' in self.theming: text_actual_area_rect.width = self.theming['text_width'] if 'text_height' in self.theming: text_actual_area_rect.height = self.theming['text_height'] text_shadow_data = (0, 0, 0, pygame.Color('#10101070'), False) if 'text_shadow' in self.theming: text_shadow_data = self.theming['text_shadow'] text_chunk = TextLineChunkFTFont( self.theming['text'], self.theming['font'], underlined=False, colour=pygame.Color('#FFFFFFFF'), using_default_text_colour=True, bg_colour=pygame.Color('#00000000'), text_shadow_data=text_shadow_data, max_dimensions=(text_actual_area_rect.width, text_actual_area_rect.height)) text_chunk.should_centre_from_baseline = True self.text_box_layout = TextBoxLayout(deque([text_chunk]), text_actual_area_rect, self.text_view_rect, line_spacing=1.25) if 'text_cursor_colour' in self.theming: self.text_box_layout.set_cursor_colour( self.theming['text_cursor_colour']) self.align_all_text_rows() return containing_rect_when_text_built
class DrawableShape: """ Base class for a graphical 'shape' that we can use for many different UI elements. The intent is to make it easy to switch between UI elements having normal rectangles, circles or rounded rectangles as their visual shape while having the same non-shape related functionality. :param containing_rect: The rectangle which this shape is entirely contained within (including shadows, borders etc) :param theming_parameters: A dictionary of user supplied data that alters the appearance of the shape. :param states: Names for the different states the shape can be in, each may have different sets of colours & images. :param manager: The UI manager for this UI. """ def __init__(self, containing_rect: pygame.Rect, theming_parameters: Dict, states: List[str], manager: IUIManagerInterface): self.theming = theming_parameters self.containing_rect = containing_rect.copy() self.text_view_rect: Optional[pygame.Rect] = None self.shadow_width = 0 self.border_width = 0 self.shape_corner_radius = 0 self.rounded_corner_offset = 0 if 'shadow_width' in self.theming: self.shadow_width = self.theming['shadow_width'] if 'border_width' in self.theming: self.border_width = self.theming['border_width'] if 'shape_corner_radius' in self.theming: self.shape_corner_radius = self.theming['shape_corner_radius'] self.rounded_corner_offset = int(self.shape_corner_radius - (math.sin(math.pi / 4) * self.shape_corner_radius)) self.text_box_layout: Optional[TextBoxLayout] = None self.build_text_layout() self._evaluate_contents_for_containing_rect() self.containing_rect.width = max(self.containing_rect.width, 1) self.containing_rect.height = max(self.containing_rect.height, 1) self.initial_text_layout_size = (self.containing_rect.width, self.containing_rect.height) self.states = {} for state in states: self.states[state] = DrawableShapeState(state) if 'normal' in states: self.active_state = self.states['normal'] else: raise NotImplementedError( "No 'normal' state id supplied for drawable shape") self.previous_state: Optional[DrawableShapeState] = None if 'transitions' in self.theming: self.state_transition_times = self.theming['transitions'] else: self.state_transition_times = {} self.ui_manager = manager self.shape_cache = self.ui_manager.get_theme().shape_cache self.states_to_redraw_queue = deque([]) self.need_to_clean_up = True self.should_trigger_full_rebuild = True self.time_until_full_rebuild_after_changing_size = 0.35 self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size self.click_area_shape = self.containing_rect.copy() self.border_rect = self.containing_rect.copy() self.background_rect = self.containing_rect.copy() self.base_surface: Optional[pygame.Surface] = None self.only_text_changed = False def _evaluate_contents_for_containing_rect(self): if self.containing_rect.width == -1: # check to see if we have text and a font, this won't work with HTML # text - throw a warning? # What we really need to to is process the html text layout by this # point but hold off finalising and passing default colours until later? if self.text_box_layout is not None: text_width = self.text_box_layout.layout_rect.width horiz_padding = 0 if 'text_horiz_alignment_padding' in self.theming: horiz_padding = self.theming[ 'text_horiz_alignment_padding'] # As well as the text width we want to throw in the borders, # shadows and any text padding final_width = (text_width + (2 * self.shadow_width) + (2 * self.border_width) + (2 * self.rounded_corner_offset) + (2 * horiz_padding)) self.text_view_rect.width = text_width self.text_box_layout.view_rect.width = self.text_view_rect.width self.containing_rect.width = final_width if self.containing_rect.height == -1: if self.text_box_layout is not None: text_height = self.text_box_layout.layout_rect.height vert_padding = 0 if 'text_vert_alignment_padding' in self.theming: vert_padding = self.theming['text_vert_alignment_padding'] # As well as the text height we want to throw in the borders, # shadows and any text padding final_height = (text_height + (2 * self.shadow_width) + (2 * self.border_width) + (2 * self.rounded_corner_offset) + (2 * vert_padding)) self.text_view_rect.height = text_height self.text_box_layout.view_rect.height = self.text_view_rect.height self.containing_rect.height = final_height def set_active_state(self, state_id: str): """ Changes the currently active state for the drawable shape and, if setup in the theme, creates a transition blend from the previous state to the newly active one. :param state_id: the ID of the new state to make active. """ # make sure this state is generated before we set it. # should ensure that some more rarely used states are only generated if we use them if not self.states[state_id].generated: if state_id in self.states_to_redraw_queue: self.states_to_redraw_queue.remove(state_id) self.redraw_state(state_id) if state_id in self.states and self.active_state.state_id != state_id: self.previous_state = self.active_state self.active_state = self.states[state_id] self.active_state.has_fresh_surface = True if self.previous_state is not None and ( (self.previous_state.state_id, self.active_state.state_id) in self.state_transition_times): prev_id = self.previous_state.state_id next_id = self.active_state.state_id duration = self.state_transition_times[( self.previous_state.state_id, self.active_state.state_id)] if self.previous_state.transition is None: # completely fresh transition self.active_state.transition = DrawableStateTransition( self.states, prev_id, next_id, duration) else: # check to see if we are reversing an in-progress transition. if self.previous_state.transition.start_stat_id == self.active_state.state_id: progress_time = self.previous_state.transition.remaining_time transition = DrawableStateTransition( self.states, prev_id, next_id, duration, progress=progress_time) self.active_state.transition = transition def update(self, time_delta: float): """ Updates the drawable shape to process rebuilds and update blends between states. :param time_delta: amount of time passed between now and the previous frame in seconds. """ if len(self.states_to_redraw_queue) > 0: state = self.states_to_redraw_queue.popleft() self.redraw_state(state) if self.need_to_clean_up and len(self.states_to_redraw_queue) == 0: # last state so clean up self.clean_up_temp_shapes() self.need_to_clean_up = False if self.full_rebuild_countdown > 0.0: self.full_rebuild_countdown -= time_delta if self.should_trigger_full_rebuild and self.full_rebuild_countdown <= 0.0: self.full_rebuild_on_size_change() self.active_state.update(time_delta) def full_rebuild_on_size_change(self): """ Triggered when we've changed the size of the shape and need to rebuild basically everything to account for it. """ shape_params_changed = False if 'shadow_width' in self.theming and self.shadow_width != self.theming[ 'shadow_width']: self.shadow_width = self.theming['shadow_width'] shape_params_changed = True if 'border_width' in self.theming and self.border_width != self.theming[ 'border_width']: self.border_width = self.theming['border_width'] shape_params_changed = True if ('shape_corner_radius' in self.theming and self.shape_corner_radius != self.theming['shape_corner_radius']): self.shape_corner_radius = self.theming['shape_corner_radius'] self.rounded_corner_offset = int(self.shape_corner_radius - (math.sin(math.pi / 4) * self.shape_corner_radius)) shape_params_changed = True if shape_params_changed or self.initial_text_layout_size != self.containing_rect.size: self.build_text_layout() self.should_trigger_full_rebuild = False self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size def redraw_all_states(self): """ Starts the redrawing process for all states of this shape that auto pre-generate. Redrawing is done one state at a time so will take a few loops of the game to complete if this shape has many states. """ self.states_to_redraw_queue = deque([ state_id for state_id, state in self.states.items() if state.should_auto_pregen ]) initial_state = self.states_to_redraw_queue.popleft() self.redraw_state(initial_state) def align_all_text_rows(self): """ Aligns the text drawing position correctly according to our theming options. """ # Horizontal alignment if 'text_horiz_alignment' in self.theming: if (self.theming['text_horiz_alignment'] == 'center' or self.theming['text_horiz_alignment'] not in ['left', 'right']): method = 'rect' if 'text_horiz_alignment_method' in self.theming: method = self.theming['text_horiz_alignment_method'] self.text_box_layout.horiz_center_all_rows(method) elif self.theming['text_horiz_alignment'] == 'left': self.text_box_layout.align_left_all_rows(0) else: self.text_box_layout.align_right_all_rows(0) else: method = 'rect' if 'text_horiz_alignment_method' in self.theming: method = self.theming['text_horiz_alignment_method'] self.text_box_layout.horiz_center_all_rows(method) # Vertical alignment if 'text_vert_alignment' in self.theming: if (self.theming['text_vert_alignment'] == 'center' or self.theming['text_vert_alignment'] not in ['top', 'bottom']): self.text_box_layout.vert_center_all_rows() elif self.theming['text_vert_alignment'] == 'top': self.text_box_layout.vert_align_top_all_rows(0) else: self.text_box_layout.vert_align_bottom_all_rows(0) else: self.text_box_layout.vert_center_all_rows() def get_active_state_surface(self) -> pygame.surface.Surface: """ Get the main surface from the active state. :return: The surface asked for, or the best available substitute. """ if self.active_state is not None: return self.active_state.get_surface() else: return self.ui_manager.get_universal_empty_surface() def get_surface(self, state_name: str) -> pygame.surface.Surface: """ Get the main surface from a specific state. :param state_name: The state we are trying to get the surface from. :return: The surface asked for, or the best available substitute. """ if state_name in self.states and self.states[ state_name].surface is not None: return self.states[state_name].surface elif state_name in self.states and self.states[ 'normal'].surface is not None: return self.states['normal'].surface else: return pygame.surface.Surface((0, 0), flags=pygame.SRCALPHA, depth=32) def get_fresh_surface(self) -> pygame.surface.Surface: """ Gets the surface of the active state and resets the state's 'has_fresh_surface' variable. :return: The active state's main pygame.surface.Surface. """ self.active_state.has_fresh_surface = False return self.get_active_state_surface() def has_fresh_surface(self) -> bool: """ Lets UI elements find out when a state has finished building a fresh surface for times when we have to delay it for whatever reason. :return: True if there is a freshly built surface waiting, False if the shape has not changed. """ return self.active_state.has_fresh_surface 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 build_text_layout(self): """ Build a text box layout for this drawable shape if it has some text. """ containing_rect_when_text_built = self.containing_rect.copy() # Draw any text if 'text' in self.theming and 'font' in self.theming and self.theming[ 'text'] is not None: # we need two rectangles for the text. One is has actual area the # text surface takes up, which may be larger than the displayed area, # and its position on the final surface. The other is the amount of # area of the text surface which we blit from, which may be much smaller # than the total text area. horiz_padding = 0 if 'text_horiz_alignment_padding' in self.theming: horiz_padding = self.theming['text_horiz_alignment_padding'] vert_padding = 0 if 'text_vert_alignment_padding' in self.theming: vert_padding = self.theming['text_vert_alignment_padding'] total_text_buffer = self.shadow_width + self.border_width + self.rounded_corner_offset self.text_view_rect = self.containing_rect.copy() self.text_view_rect.x = 0 self.text_view_rect.y = 0 if self.text_view_rect.width != -1: self.text_view_rect.width -= ((total_text_buffer * 2) + (2 * horiz_padding)) if self.text_view_rect.height != -1: self.text_view_rect.height -= ((total_text_buffer * 2) + (2 * vert_padding)) text_actual_area_rect = self.text_view_rect.copy() text_actual_area_rect.x = total_text_buffer + horiz_padding text_actual_area_rect.y = total_text_buffer + vert_padding if 'text_width' in self.theming: text_actual_area_rect.width = self.theming['text_width'] if 'text_height' in self.theming: text_actual_area_rect.height = self.theming['text_height'] text_shadow_data = (0, 0, 0, pygame.Color('#10101070'), False) if 'text_shadow' in self.theming: text_shadow_data = self.theming['text_shadow'] text_chunk = TextLineChunkFTFont( self.theming['text'], self.theming['font'], underlined=False, colour=pygame.Color('#FFFFFFFF'), using_default_text_colour=True, bg_colour=pygame.Color('#00000000'), text_shadow_data=text_shadow_data, max_dimensions=(text_actual_area_rect.width, text_actual_area_rect.height)) text_chunk.should_centre_from_baseline = True self.text_box_layout = TextBoxLayout(deque([text_chunk]), text_actual_area_rect, self.text_view_rect, line_spacing=1.25) if 'text_cursor_colour' in self.theming: self.text_box_layout.set_cursor_colour( self.theming['text_cursor_colour']) self.align_all_text_rows() return containing_rect_when_text_built 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 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_text(self, text: str): """ Set the visible text that the drawable shape has on it. This call will build a text layout and then redraw the final shape with the new, laid out text on top. :param text: the new string of text to stick on the shape. """ self.theming['text'] = text self.build_text_layout() self.redraw_all_states() def set_text_alpha(self, alpha: int): """ Set the alpha of just the text and redraw the shape with the new text on top. :param alpha: the alpha to set. """ self.text_box_layout.set_alpha(alpha) self.redraw_state(self.active_state.state_id, add_text=False) self.finalise_text(self.active_state.state_id, only_text_changed=True) def redraw_active_state_no_text(self): """ Redraw the currently active state with no text. """ self.redraw_state(self.active_state.state_id, add_text=False) def finalise_text_onto_active_state(self): """ Lets us draw the active state with no text and then paste the finalised surface from the text layout on top. Handy if we are doing some text effects in the text layout we don't want to lose by recreating the text from scratch. """ self.redraw_state(self.active_state.state_id, add_text=False) self.finalise_text(self.active_state.state_id, only_text_changed=True) def insert_text(self, text: str, layout_index: int, parser: Optional[HTMLParser] = None): """ Update the theming when we insert text, then pass down to the layout to do the actual inserting. :param text: the text to insert :param layout_index: where to insert it :param parser: an optional parser :return: """ self.theming['text'] = (self.theming['text'][:layout_index] + text + self.theming['text'][layout_index:]) self.text_box_layout.insert_text(text, layout_index, parser) def toggle_text_cursor(self): """ Toggle the edit text cursor/carat between visible and invisible. Usually this is run to make the cursor appear to flash so it catches user attention. """ if self.text_box_layout is not None: self.text_box_layout.toggle_cursor() self.apply_active_text_changes() self.active_state.has_fresh_surface = True def redraw_state(self, state_str: str, add_text: bool = True): """ This method is declared for derived classes to implement but has no default implementation. :param add_text: :param state_str: The ID/name of the state to redraw. """ def clean_up_temp_shapes(self): """ This method is declared for derived classes to implement but has no default implementation. """ def collide_point(self, point: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ This method is declared for derived classes to implement but has no default implementation. :param point: A point to collide with this shape. """ def set_position(self, point: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """ This method is declared for derived classes to implement but has no default implementation. :param point: A point to set this shapes position to. """ def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): """